Compare commits
7 Commits
main
...
5298fb9f60
| Author | SHA1 | Date | |
|---|---|---|---|
| 5298fb9f60 | |||
| c95a6517e9 | |||
| 79f523e9cb | |||
| 90f3d66b22 | |||
| 2b48ade2f1 | |||
| b0fdf03cc6 | |||
|
|
349c7009c6 |
@@ -2,7 +2,7 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/png" href="/favicon_ship.png" />
|
||||
<link rel="icon" type="image/svg" href="/favicon_ship.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Белые ночи</title>
|
||||
</head>
|
||||
|
||||
3409
package-lock.json
generated
@@ -3,6 +3,7 @@
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"license": "UNLICENSED",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
@@ -23,6 +24,7 @@
|
||||
"@tailwindcss/vite": "^4.1.8",
|
||||
"axios": "^1.9.0",
|
||||
"easymde": "^2.20.0",
|
||||
"i18n-iso-countries": "^7.14.0",
|
||||
"lucide-react": "^0.511.0",
|
||||
"mobx": "^6.13.7",
|
||||
"mobx-react-lite": "^4.1.0",
|
||||
@@ -30,11 +32,10 @@
|
||||
"path": "^0.12.7",
|
||||
"pixi.js": "^8.10.1",
|
||||
"react": "^19.1.0",
|
||||
"react-colorful": "^5.6.1",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-dropzone": "^14.3.8",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-photo-sphere-viewer": "^6.2.3",
|
||||
"react-router": "^7.9.4",
|
||||
"react-router-dom": "^7.6.1",
|
||||
"react-simplemde-editor": "^5.2.0",
|
||||
"react-toastify": "^11.0.5",
|
||||
@@ -53,9 +54,11 @@
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.19",
|
||||
"globals": "^16.0.0",
|
||||
"rollup-plugin-visualizer": "^6.0.5",
|
||||
"typescript": "~5.8.3",
|
||||
"typescript-eslint": "^8.30.1",
|
||||
"vite": "^6.3.5"
|
||||
"vite": "^6.3.5",
|
||||
"vite-plugin-svgr": "^4.5.0"
|
||||
},
|
||||
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 176 KiB |
BIN
public/GET.png
|
Before Width: | Height: | Size: 6.4 KiB |
|
Before Width: | Height: | Size: 750 B |
|
Before Width: | Height: | Size: 2.3 KiB |
3
public/favicon_ship.svg
Normal file
|
After Width: | Height: | Size: 7.2 KiB |
21
public/sight_icon.svg
Normal file
@@ -0,0 +1,21 @@
|
||||
<svg width="53" height="49" viewBox="0 0 53 49" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 19.9296H52.7174V15.9968L26.3662 0L0 15.9968V19.9296ZM26.3659 3.75713L4.75331 16.8616H47.9636L26.3659 3.75713Z" fill="#A6A6A6"/>
|
||||
<path d="M52.7174 45.4072H0V48.3587H52.7174V45.4072Z" fill="#A6A6A6"/>
|
||||
<path d="M50.0742 41.4756H2.64355V44.427H50.0742V41.4756Z" fill="#A6A6A6"/>
|
||||
<path d="M9.46312 21.6035H5.49805V39.0244H9.46312V21.6035Z" fill="#A6A6A6"/>
|
||||
<path d="M11.4448 39.4316H3.51465V40.5827H11.4448V39.4316Z" fill="#A6A6A6"/>
|
||||
<path d="M4.40104 20.6592C4.10066 20.6592 3.86035 20.8953 3.86035 21.1904C3.86035 21.4856 4.10066 21.7217 4.40104 21.7217C4.70143 21.7217 4.94173 21.4856 4.94173 21.1904H10.0182C10.0182 21.4856 10.2585 21.7217 10.5589 21.7217C10.8593 21.7217 11.0996 21.4856 11.0996 21.1904C11.0996 20.8953 10.8593 20.6592 10.5589 20.6592H4.40104Z" fill="#A6A6A6"/>
|
||||
<path d="M22.0979 21.6035H18.1328V39.0244H22.0979V21.6035Z" fill="#A6A6A6"/>
|
||||
<path d="M24.0815 39.4316H16.1514V40.5827H24.0815V39.4316Z" fill="#A6A6A6"/>
|
||||
<path d="M17.0358 20.6592C16.7354 20.6592 16.4951 20.8953 16.4951 21.1904C16.4951 21.4856 16.7354 21.7217 17.0358 21.7217C17.3362 21.7217 17.5765 21.4856 17.5765 21.1904H22.653C22.653 21.4856 22.8933 21.7217 23.1937 21.7217C23.4941 21.7217 23.7344 21.4856 23.7344 21.1904C23.7344 20.8953 23.4941 20.6592 23.1937 20.6592H17.0358Z" fill="#A6A6A6"/>
|
||||
<path d="M34.7414 21.6035H30.7764V39.0244H34.7414V21.6035Z" fill="#A6A6A6"/>
|
||||
<path d="M36.7231 39.4316H28.793V40.5827H36.7231V39.4316Z" fill="#A6A6A6"/>
|
||||
<path d="M29.6794 20.6592C29.379 20.6592 29.1387 20.8953 29.1387 21.1904C29.1387 21.4856 29.379 21.7217 29.6794 21.7217C29.9797 21.7217 30.2201 21.4856 30.2201 21.1904H35.2965C35.2965 21.4856 35.5369 21.7217 35.8372 21.7217C36.1376 21.7217 36.3779 21.4856 36.3779 21.1904C36.3779 20.8953 36.1376 20.6592 35.8372 20.6592H29.6794Z" fill="#A6A6A6"/>
|
||||
<path d="M47.3762 21.6045H43.4111V39.0254H47.3762V21.6045Z" fill="#A6A6A6"/>
|
||||
<path d="M49.3598 39.4316H41.4297V40.5827H49.3598V39.4316Z" fill="#A6A6A6"/>
|
||||
<path d="M42.3141 20.6592C42.0137 20.6592 41.7734 20.8953 41.7734 21.1904C41.7734 21.4856 42.0137 21.7217 42.3141 21.7217C42.6145 21.7217 42.8548 21.4856 42.8548 21.1904H47.9313C47.9313 21.4856 48.1716 21.7217 48.472 21.7217C48.7724 21.7217 49.0127 21.4856 49.0127 21.1904C49.0127 20.8953 48.7724 20.6592 48.472 20.6592H42.3141Z" fill="#A6A6A6"/>
|
||||
<path d="M26.8478 9.42308C26.8478 9.18696 26.6151 8.99512 26.3297 8.99512C26.0443 8.99512 25.8115 9.18696 25.8115 9.42308V9.76249C25.8115 10.0429 26.0443 10.2716 26.3297 10.2716C26.6151 10.2716 26.8478 10.0429 26.8478 9.76249V9.42308Z" fill="#A6A6A6"/>
|
||||
<path d="M19.5098 11.6514C19.2695 11.6514 19.0742 11.8801 19.0742 12.1679C19.0742 12.4483 19.2695 12.6844 19.5098 12.6844H20.4109C20.6963 12.6844 20.9366 12.4556 20.9366 12.1679C20.9366 11.8875 20.7038 11.6514 20.4109 11.6514H19.5098Z" fill="#A6A6A6"/>
|
||||
<path d="M32.2022 11.6514C31.9619 11.6514 31.7666 11.8801 31.7666 12.1679C31.7666 12.4483 31.9619 12.6844 32.2022 12.6844H33.1033C33.3887 12.6844 33.629 12.4556 33.629 12.1679C33.629 11.8875 33.3962 11.6514 33.1033 11.6514H32.2022Z" fill="#A6A6A6"/>
|
||||
<path d="M27.6188 11.1644L26.973 10.7586C26.973 10.7586 26.8979 10.7217 26.8528 10.7217H25.8015C25.7564 10.7217 25.7189 10.7364 25.6813 10.7586L25.0355 11.1644C24.9679 11.2087 24.9304 11.2751 24.9304 11.3489V11.8211C24.9304 11.8654 24.9454 11.9096 24.9679 11.9465L25.5311 12.7656C25.5762 12.832 25.5837 12.9057 25.5537 12.9795L24.9454 14.3372C24.9454 14.3372 24.9229 14.3962 24.9229 14.4257V15.776C24.9229 15.9015 25.0205 15.9974 25.1481 15.9974H27.4911C27.6188 15.9974 27.7164 15.9015 27.7164 15.776V14.4257C27.7164 14.4257 27.7164 14.3667 27.6939 14.3372L27.0856 12.9795C27.0556 12.9131 27.0631 12.832 27.1081 12.7656L27.6714 11.9465C27.6714 11.9465 27.7089 11.8654 27.7089 11.8211V11.3489C27.7089 11.2751 27.6714 11.2013 27.6038 11.1644H27.6188Z" fill="#A6A6A6"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.9 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
Before Width: | Height: | Size: 1.5 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
|
||||
|
Before Width: | Height: | Size: 4.0 KiB |
@@ -1,5 +1,6 @@
|
||||
@import "tailwindcss";
|
||||
@plugin "@tailwindcss/typography";
|
||||
|
||||
button {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@@ -6,12 +6,13 @@ import React, {
|
||||
useMemo,
|
||||
} from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { Map, View, Overlay, MapBrowserEvent } from "ol";
|
||||
import { Map as OLMap, View, Overlay, MapBrowserEvent } from "ol";
|
||||
import TileLayer from "ol/layer/Tile";
|
||||
import OSM from "ol/source/OSM";
|
||||
import VectorLayer from "ol/layer/Vector";
|
||||
import VectorSource, { VectorSourceEvent } from "ol/source/Vector";
|
||||
import Cluster from "ol/source/Cluster";
|
||||
|
||||
import {
|
||||
Draw,
|
||||
Modify,
|
||||
@@ -47,6 +48,9 @@ import {
|
||||
InfoIcon,
|
||||
X,
|
||||
Loader2,
|
||||
EyeOff,
|
||||
Eye,
|
||||
Map as MapIcon,
|
||||
} from "lucide-react";
|
||||
import { toast } from "react-toastify";
|
||||
import { singleClick, doubleClick } from "ol/events/condition";
|
||||
@@ -135,6 +139,7 @@ interface ApiRoute {
|
||||
interface ApiStation {
|
||||
id: number;
|
||||
name: string;
|
||||
description?: string;
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
city_id: number;
|
||||
@@ -161,14 +166,71 @@ export type SortType =
|
||||
| "updated_asc"
|
||||
| "updated_desc";
|
||||
|
||||
// --- HIDDEN ROUTES STORAGE ---
|
||||
const HIDDEN_ROUTES_KEY = "mapHiddenRoutes";
|
||||
const HIDE_SIGHTS_BY_HIDDEN_ROUTES_KEY = "mapHideSightsByHiddenRoutes";
|
||||
|
||||
const getStoredHiddenRoutes = (): Set<number> => {
|
||||
try {
|
||||
const stored = localStorage.getItem(HIDDEN_ROUTES_KEY);
|
||||
if (stored) {
|
||||
const routes = JSON.parse(stored);
|
||||
if (
|
||||
Array.isArray(routes) &&
|
||||
routes.every((id) => typeof id === "number")
|
||||
) {
|
||||
return new Set(routes);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("Failed to parse stored hidden routes:", error);
|
||||
}
|
||||
return new Set();
|
||||
};
|
||||
|
||||
const saveHiddenRoutes = (hiddenRoutes: Set<number>): void => {
|
||||
try {
|
||||
localStorage.setItem(
|
||||
HIDDEN_ROUTES_KEY,
|
||||
JSON.stringify(Array.from(hiddenRoutes))
|
||||
);
|
||||
} catch (error) {
|
||||
console.warn("Failed to save hidden routes:", error);
|
||||
}
|
||||
};
|
||||
|
||||
class MapStore {
|
||||
constructor() {
|
||||
makeAutoObservable(this);
|
||||
// Загружаем скрытые маршруты из localStorage при инициализации
|
||||
this.hiddenRoutes = getStoredHiddenRoutes();
|
||||
// Загружаем настройку скрытия достопримечательностей
|
||||
try {
|
||||
const stored = localStorage.getItem(HIDE_SIGHTS_BY_HIDDEN_ROUTES_KEY);
|
||||
this.hideSightsByHiddenRoutes = stored
|
||||
? JSON.parse(stored) === true
|
||||
: false;
|
||||
} catch (e) {
|
||||
this.hideSightsByHiddenRoutes = false;
|
||||
}
|
||||
}
|
||||
|
||||
routes: ApiRoute[] = [];
|
||||
stations: ApiStation[] = [];
|
||||
sights: ApiSight[] = [];
|
||||
hiddenRoutes: Set<number>;
|
||||
hideSightsByHiddenRoutes: boolean = false;
|
||||
routeStationsCache: Map<number, number[]> = new Map(); // Кэш станций для маршрутов
|
||||
routeSightsCache: Map<number, number[]> = new Map(); // Кэш достопримечательностей для маршрутов
|
||||
setHideSightsByHiddenRoutes(val: boolean) {
|
||||
this.hideSightsByHiddenRoutes = val;
|
||||
try {
|
||||
localStorage.setItem(
|
||||
HIDE_SIGHTS_BY_HIDDEN_ROUTES_KEY,
|
||||
JSON.stringify(!!val)
|
||||
);
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
// НОВЫЕ ПОЛЯ ДЛЯ СОРТИРОВКИ
|
||||
stationSort: SortType = "name_asc";
|
||||
@@ -296,12 +358,23 @@ class MapStore {
|
||||
|
||||
get filteredSights(): ApiSight[] {
|
||||
const selectedCityId = selectedCityStore.selectedCityId;
|
||||
if (!selectedCityId) {
|
||||
return this.sortedSights;
|
||||
const cityFiltered = !selectedCityId
|
||||
? this.sortedSights
|
||||
: this.sortedSights.filter((sight) => sight.city_id === selectedCityId);
|
||||
|
||||
if (!this.hideSightsByHiddenRoutes || this.hiddenRoutes.size === 0) {
|
||||
return cityFiltered;
|
||||
}
|
||||
return this.sortedSights.filter(
|
||||
(sight) => sight.city_id === selectedCityId
|
||||
);
|
||||
|
||||
// Собираем все достопримечательности, связанные со скрытыми маршрутами
|
||||
const hiddenSightIds = new Set<number>();
|
||||
this.hiddenRoutes.forEach((routeId) => {
|
||||
const sightIds = this.routeSightsCache.get(routeId) || [];
|
||||
sightIds.forEach((id) => hiddenSightIds.add(id));
|
||||
});
|
||||
|
||||
// Фильтруем достопримечательности, исключая привязанные к скрытым маршрутам
|
||||
return cityFiltered.filter((s) => !hiddenSightIds.has(s.id));
|
||||
}
|
||||
|
||||
getRoutes = async () => {
|
||||
@@ -323,6 +396,54 @@ class MapStore {
|
||||
this.routes = this.routes.sort((a, b) =>
|
||||
a.route_number.localeCompare(b.route_number)
|
||||
);
|
||||
|
||||
// Предзагружаем станции для всех маршрутов и кэшируем их
|
||||
await this.preloadRouteStations(routesIds);
|
||||
// Предзагружаем достопримечательности для всех маршрутов
|
||||
await this.preloadRouteSights(routesIds);
|
||||
};
|
||||
|
||||
preloadRouteStations = async (routesIds: number[]) => {
|
||||
console.log(
|
||||
`[MapStore] Preloading stations for ${routesIds.length} routes`
|
||||
);
|
||||
const stationPromises = routesIds.map(async (routeId) => {
|
||||
try {
|
||||
const stationsResponse = await languageInstance("ru").get(
|
||||
`/route/${routeId}/station`
|
||||
);
|
||||
const stationIds = stationsResponse.data.map((s: any) => s.id);
|
||||
this.routeStationsCache.set(routeId, stationIds);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`Failed to preload stations for route ${routeId}:`,
|
||||
error
|
||||
);
|
||||
}
|
||||
});
|
||||
await Promise.all(stationPromises);
|
||||
console.log(
|
||||
`[MapStore] Preloaded stations for ${this.routeStationsCache.size} routes`
|
||||
);
|
||||
};
|
||||
|
||||
preloadRouteSights = async (routesIds: number[]) => {
|
||||
console.log(`[MapStore] Preloading sights for ${routesIds.length} routes`);
|
||||
const sightPromises = routesIds.map(async (routeId) => {
|
||||
try {
|
||||
const sightsResponse = await languageInstance("ru").get(
|
||||
`/route/${routeId}/sight`
|
||||
);
|
||||
const sightIds = sightsResponse.data.map((s: any) => s.id);
|
||||
this.routeSightsCache.set(routeId, sightIds);
|
||||
} catch (error) {
|
||||
console.error(`Failed to preload sights for route ${routeId}:`, error);
|
||||
}
|
||||
});
|
||||
await Promise.all(sightPromises);
|
||||
console.log(
|
||||
`[MapStore] Preloaded sights for ${this.routeSightsCache.size} routes`
|
||||
);
|
||||
};
|
||||
|
||||
getStations = async () => {
|
||||
@@ -429,8 +550,8 @@ class MapStore {
|
||||
rotate: 0,
|
||||
route_direction: false,
|
||||
route_sys_number: route_number,
|
||||
scale_max: 0,
|
||||
scale_min: 0,
|
||||
scale_max: 100,
|
||||
scale_min: 10,
|
||||
};
|
||||
|
||||
await routeStore.createRoute(routeData);
|
||||
@@ -640,40 +761,6 @@ const saveActiveSection = (section: string | null): void => {
|
||||
}
|
||||
};
|
||||
|
||||
// --- SVG ICONS ---
|
||||
const EditIcon = () => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="h-4 w-4 mr-1 sm:mr-2"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
const LineIconSvg = () => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="h-4 w-4 mr-1 sm:mr-2"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M3.055 11H5a2 2 0 012 2v1a2 2 0 002 2h10a2 2 0 002-2v-1a2 2 0 012-2h1.945M7.732 4.064A2.5 2.5 0 105.23 6.24m13.54 0a2.5 2.5 0 10-2.502-2.176M12 16.05V21m0-17.948V3"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
// --- TYPE DEFINITIONS ---
|
||||
interface MapServiceConfig {
|
||||
target: HTMLElement;
|
||||
@@ -684,7 +771,7 @@ interface MapServiceConfig {
|
||||
type FeatureType = "station" | "route" | "sight";
|
||||
|
||||
class MapService {
|
||||
private map: Map | null;
|
||||
private map: OLMap | null;
|
||||
public pointSource: VectorSource<Feature<Point>>;
|
||||
public lineSource: VectorSource<Feature<LineString>>;
|
||||
public clusterLayer: VectorLayer<Cluster>; // Public for the deselect handler
|
||||
@@ -976,7 +1063,7 @@ class MapService {
|
||||
const initialCenter = storedPosition?.center || config.center;
|
||||
const initialZoom = storedPosition?.zoom || config.zoom;
|
||||
|
||||
this.map = new Map({
|
||||
this.map = new OLMap({
|
||||
target: config.target,
|
||||
layers: [
|
||||
new TileLayer({ source: new OSM() }),
|
||||
@@ -1284,32 +1371,17 @@ class MapService {
|
||||
}
|
||||
}
|
||||
|
||||
// Стандартная логика выделения для одиночных объектов (или с Ctrl)
|
||||
// При Ctrl+клик сохраняем предыдущие выделения и добавляем/удаляем только изменённые
|
||||
// При обычном клике создаём новый набор
|
||||
const newSelectedIds = ctrlKey
|
||||
? new Set(this.selectedIds)
|
||||
: new Set<string | number>();
|
||||
|
||||
// Добавляем новые выбранные элементы
|
||||
e.selected.forEach((feature) => {
|
||||
const originalFeatures = feature.get("features");
|
||||
let targetId: string | number | undefined;
|
||||
|
||||
if (originalFeatures && originalFeatures.length > 0) {
|
||||
// Это фича из кластера (может быть и одна)
|
||||
targetId = originalFeatures[0].getId();
|
||||
} else {
|
||||
// Это линия или что-то не из кластера
|
||||
targetId = feature.getId();
|
||||
}
|
||||
|
||||
if (targetId !== undefined) {
|
||||
newSelectedIds.add(targetId);
|
||||
}
|
||||
});
|
||||
|
||||
e.deselected.forEach((feature) => {
|
||||
const originalFeatures = feature.get("features");
|
||||
let targetId: string | number | undefined;
|
||||
|
||||
if (originalFeatures && originalFeatures.length > 0) {
|
||||
targetId = originalFeatures[0].getId();
|
||||
} else {
|
||||
@@ -1317,10 +1389,36 @@ class MapService {
|
||||
}
|
||||
|
||||
if (targetId !== undefined) {
|
||||
newSelectedIds.delete(targetId);
|
||||
// При Ctrl+клик: если элемент уже был выбран, снимаем его (toggle)
|
||||
// Если не был выбран, добавляем
|
||||
if (ctrlKey && newSelectedIds.has(targetId)) {
|
||||
newSelectedIds.delete(targetId);
|
||||
} else {
|
||||
newSelectedIds.add(targetId);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// При Ctrl+клик игнорируем deselected, так как Select interaction может снимать
|
||||
// предыдущие выделения, но мы хотим их сохранить
|
||||
// При обычном клике удаляем deselected элементы
|
||||
if (!ctrlKey) {
|
||||
e.deselected.forEach((feature) => {
|
||||
const originalFeatures = feature.get("features");
|
||||
let targetId: string | number | undefined;
|
||||
|
||||
if (originalFeatures && originalFeatures.length > 0) {
|
||||
targetId = originalFeatures[0].getId();
|
||||
} else {
|
||||
targetId = feature.getId();
|
||||
}
|
||||
|
||||
if (targetId !== undefined) {
|
||||
newSelectedIds.delete(targetId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this.setSelectedIds(newSelectedIds);
|
||||
});
|
||||
|
||||
@@ -1406,8 +1504,33 @@ class MapService {
|
||||
const filteredSights = mapStore.filteredSights;
|
||||
const filteredRoutes = mapStore.filteredRoutes;
|
||||
|
||||
console.log(
|
||||
`[loadFeaturesFromApi] Loading with ${mapStore.hiddenRoutes.size} hidden routes`
|
||||
);
|
||||
|
||||
// Собираем все станции видимых маршрутов из кэша
|
||||
const stationsInVisibleRoutes = new Set<number>();
|
||||
filteredRoutes
|
||||
.filter((route) => !mapStore.hiddenRoutes.has(route.id))
|
||||
.forEach((route) => {
|
||||
const stationIds = mapStore.routeStationsCache.get(route.id) || [];
|
||||
stationIds.forEach((id) => stationsInVisibleRoutes.add(id));
|
||||
});
|
||||
|
||||
console.log(
|
||||
`[loadFeaturesFromApi] Found ${stationsInVisibleRoutes.size} stations in visible routes, total stations: ${filteredStations.length}`
|
||||
);
|
||||
|
||||
let skippedStations = 0;
|
||||
filteredStations.forEach((station) => {
|
||||
if (station.longitude == null || station.latitude == null) return;
|
||||
|
||||
// Пропускаем станции, которые принадлежат только скрытым маршрутам
|
||||
if (!stationsInVisibleRoutes.has(station.id)) {
|
||||
skippedStations++;
|
||||
return;
|
||||
}
|
||||
|
||||
const point = new Point(
|
||||
transform(
|
||||
[station.longitude, station.latitude],
|
||||
@@ -1438,6 +1561,10 @@ class MapService {
|
||||
|
||||
filteredRoutes.forEach((route) => {
|
||||
if (!route.path || route.path.length === 0) return;
|
||||
|
||||
// Пропускаем скрытые маршруты
|
||||
if (mapStore.hiddenRoutes.has(route.id)) return;
|
||||
|
||||
const coordinates = route.path
|
||||
.filter((c) => c && c[0] != null && c[1] != null)
|
||||
.map((c: [number, number]) =>
|
||||
@@ -1456,6 +1583,10 @@ class MapService {
|
||||
lineFeatures.push(lineFeature);
|
||||
});
|
||||
|
||||
console.log(
|
||||
`[loadFeaturesFromApi] Skipped ${skippedStations} stations (belonging only to hidden routes)`
|
||||
);
|
||||
|
||||
this.pointSource.addFeatures(pointFeatures);
|
||||
this.lineSource.addFeatures(lineFeatures);
|
||||
|
||||
@@ -1913,10 +2044,14 @@ class MapService {
|
||||
this.selectInteraction.getFeatures().clear();
|
||||
ids.forEach((id) => {
|
||||
const lineFeature = this.lineSource.getFeatureById(id);
|
||||
if (lineFeature) this.selectInteraction.getFeatures().push(lineFeature);
|
||||
if (lineFeature) {
|
||||
this.selectInteraction.getFeatures().push(lineFeature);
|
||||
}
|
||||
|
||||
const pointFeature = this.pointSource.getFeatureById(id);
|
||||
if (pointFeature) this.selectInteraction.getFeatures().push(pointFeature);
|
||||
if (pointFeature) {
|
||||
this.selectInteraction.getFeatures().push(pointFeature);
|
||||
}
|
||||
});
|
||||
|
||||
this.modifyInteraction.setActive(
|
||||
@@ -1948,7 +2083,7 @@ class MapService {
|
||||
if (this.mode === "lasso") this.deactivateLasso();
|
||||
else this.activateLasso();
|
||||
}
|
||||
public getMap(): Map | null {
|
||||
public getMap(): OLMap | null {
|
||||
return this.map;
|
||||
}
|
||||
|
||||
@@ -2130,7 +2265,7 @@ const MapControls: React.FC<MapControlsProps> = ({
|
||||
mode: "edit",
|
||||
title: "Редактировать",
|
||||
longTitle: "Редактирование",
|
||||
icon: <EditIcon />,
|
||||
icon: <Pencil size={16} className="mr-1 sm:mr-2" />,
|
||||
action: () => mapService.activateEditMode(),
|
||||
},
|
||||
{
|
||||
@@ -2151,7 +2286,7 @@ const MapControls: React.FC<MapControlsProps> = ({
|
||||
mode: "drawing-route",
|
||||
title: "Маршрут",
|
||||
longTitle: "Добавить маршрут (Правый клик для завершения)",
|
||||
icon: <LineIconSvg />,
|
||||
icon: <RouteIcon size={16} className="mr-1 sm:mr-2" />,
|
||||
action: () => mapService.startDrawing("LineString", "route"),
|
||||
},
|
||||
|
||||
@@ -2241,6 +2376,7 @@ const MapSightbar: React.FC<MapSightbarProps> = observer(
|
||||
)
|
||||
),
|
||||
name: station.name,
|
||||
description: station.description || "",
|
||||
});
|
||||
feature.setId(`station-${station.id}`);
|
||||
feature.set("featureType", "station");
|
||||
@@ -2296,11 +2432,26 @@ const MapSightbar: React.FC<MapSightbarProps> = observer(
|
||||
}, [allFeatures, searchQuery]);
|
||||
|
||||
const handleFeatureClick = useCallback(
|
||||
(id: string | number) => {
|
||||
(id: string | number, event?: React.MouseEvent) => {
|
||||
if (!mapService) return;
|
||||
mapService.selectFeature(id);
|
||||
const ctrlKey = event?.ctrlKey || event?.metaKey;
|
||||
|
||||
if (ctrlKey) {
|
||||
// Множественный выбор: добавляем к существующему
|
||||
const newSet = new Set(selectedIds);
|
||||
if (newSet.has(id)) {
|
||||
newSet.delete(id);
|
||||
} else {
|
||||
newSet.add(id);
|
||||
}
|
||||
setSelectedIds(newSet);
|
||||
mapService.setSelectedIds(newSet);
|
||||
} else {
|
||||
// Одиночный выбор: используем стандартный метод
|
||||
mapService.selectFeature(id);
|
||||
}
|
||||
},
|
||||
[mapService]
|
||||
[mapService, selectedIds, setSelectedIds]
|
||||
);
|
||||
|
||||
const handleDeleteFeature = useCallback(
|
||||
@@ -2346,6 +2497,217 @@ const MapSightbar: React.FC<MapSightbarProps> = observer(
|
||||
[navigate]
|
||||
);
|
||||
|
||||
const handleHideRoute = useCallback(
|
||||
async (routeId: string | number) => {
|
||||
if (!mapService) return;
|
||||
|
||||
const numericRouteId = parseInt(String(routeId).split("-")[1], 10);
|
||||
if (isNaN(numericRouteId)) return;
|
||||
|
||||
const isHidden = mapStore.hiddenRoutes.has(numericRouteId);
|
||||
console.log(
|
||||
`[handleHideRoute] Route ${numericRouteId}, isHidden: ${isHidden}`
|
||||
);
|
||||
|
||||
try {
|
||||
if (isHidden) {
|
||||
console.log(`[handleHideRoute] Showing route ${numericRouteId}`);
|
||||
// Показываем маршрут обратно
|
||||
const route = mapStore.routes.find((r) => r.id === numericRouteId);
|
||||
if (!route) {
|
||||
console.warn(
|
||||
`[handleHideRoute] Route ${numericRouteId} not found in mapStore`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const projection = mapService.getMap()?.getView().getProjection();
|
||||
if (!projection) {
|
||||
console.error(`[handleHideRoute] Failed to get map projection`);
|
||||
return;
|
||||
}
|
||||
console.log(
|
||||
`[handleHideRoute] Route ${numericRouteId} (${route.route_number}) found, showing`
|
||||
);
|
||||
|
||||
// Показываем сам маршрут
|
||||
const coordinates = route.path
|
||||
.filter((c) => c && c[0] != null && c[1] != null)
|
||||
.map((c: [number, number]) =>
|
||||
transform([c[1], c[0]], "EPSG:4326", projection)
|
||||
);
|
||||
|
||||
if (coordinates.length > 0) {
|
||||
const line = new LineString(coordinates);
|
||||
const lineFeature = new Feature({
|
||||
geometry: line,
|
||||
name: route.route_number,
|
||||
});
|
||||
lineFeature.setId(routeId);
|
||||
lineFeature.set("featureType", "route");
|
||||
mapService.lineSource.addFeature(lineFeature);
|
||||
console.log(`[handleHideRoute] Added route line to map`);
|
||||
} else {
|
||||
console.warn(
|
||||
`[handleHideRoute] No valid coordinates for route ${numericRouteId}`
|
||||
);
|
||||
}
|
||||
|
||||
// Получаем станции текущего маршрута из кэша
|
||||
const routeStationIds =
|
||||
mapStore.routeStationsCache.get(numericRouteId) || [];
|
||||
console.log(
|
||||
`[handleHideRoute] Route ${numericRouteId} has ${routeStationIds.length} stations`
|
||||
);
|
||||
|
||||
// Получаем все маршруты для проверки
|
||||
const allRouteIds = mapStore.routes.map((r) => r.id);
|
||||
|
||||
// Исключаем скрытые маршруты из проверки
|
||||
const visibleRouteIds = allRouteIds.filter(
|
||||
(id: number) =>
|
||||
id !== numericRouteId && !mapStore.hiddenRoutes.has(id)
|
||||
);
|
||||
console.log(
|
||||
`[handleHideRoute] Checking against ${visibleRouteIds.length} visible routes (excluding hidden)`
|
||||
);
|
||||
|
||||
// Собираем все станции видимых маршрутов из кэша
|
||||
const stationsInVisibleRoutes = new Set<number>();
|
||||
visibleRouteIds.forEach((otherRouteId) => {
|
||||
const stationIds =
|
||||
mapStore.routeStationsCache.get(otherRouteId) || [];
|
||||
stationIds.forEach((id: number) =>
|
||||
stationsInVisibleRoutes.add(id)
|
||||
);
|
||||
});
|
||||
|
||||
console.log(
|
||||
`[handleHideRoute] Found ${stationsInVisibleRoutes.size} unique stations in visible routes`
|
||||
);
|
||||
|
||||
// Показываем станции, которые не используются в других ВИДИМЫХ маршрутах
|
||||
const stationsToShow = routeStationIds.filter(
|
||||
(id: number) => !stationsInVisibleRoutes.has(id)
|
||||
);
|
||||
|
||||
// Показываем станции на карте
|
||||
for (const stationId of stationsToShow) {
|
||||
const station = mapStore.stations.find((s) => s.id === stationId);
|
||||
if (!station) continue;
|
||||
|
||||
const point = new Point(
|
||||
transform(
|
||||
[station.longitude, station.latitude],
|
||||
"EPSG:4326",
|
||||
projection
|
||||
)
|
||||
);
|
||||
const feature = new Feature({
|
||||
geometry: point,
|
||||
name: station.name,
|
||||
});
|
||||
feature.setId(`station-${station.id}`);
|
||||
feature.set("featureType", "station");
|
||||
|
||||
// Добавляем станцию только если её еще нет на карте
|
||||
const existingFeature = mapService.pointSource.getFeatureById(
|
||||
`station-${station.id}`
|
||||
);
|
||||
if (!existingFeature) {
|
||||
mapService.pointSource.addFeature(feature);
|
||||
}
|
||||
}
|
||||
|
||||
// Удаляем из скрытых
|
||||
mapStore.hiddenRoutes.delete(numericRouteId);
|
||||
saveHiddenRoutes(mapStore.hiddenRoutes); // Сохраняем в localStorage
|
||||
console.log(
|
||||
`[handleHideRoute] Removed ${numericRouteId} from hiddenRoutes, stations to show: ${stationsToShow.length}`
|
||||
);
|
||||
} else {
|
||||
// Скрываем маршрут
|
||||
console.log(`[handleHideRoute] Hiding route ${numericRouteId}`);
|
||||
|
||||
// Получаем станции текущего маршрута из кэша
|
||||
const routeStationIds =
|
||||
mapStore.routeStationsCache.get(numericRouteId) || [];
|
||||
console.log(
|
||||
`[handleHideRoute] Route ${numericRouteId} has ${routeStationIds.length} stations`
|
||||
);
|
||||
|
||||
// Получаем все маршруты для проверки
|
||||
const allRouteIds = mapStore.routes.map((r) => r.id);
|
||||
|
||||
// Исключаем скрытые маршруты из проверки
|
||||
const visibleRouteIds = allRouteIds.filter(
|
||||
(id: number) =>
|
||||
id !== numericRouteId && !mapStore.hiddenRoutes.has(id)
|
||||
);
|
||||
console.log(
|
||||
`[handleHideRoute] Checking against ${visibleRouteIds.length} visible routes (excluding hidden)`
|
||||
);
|
||||
|
||||
// Собираем все станции видимых маршрутов из кэша
|
||||
const stationsInVisibleRoutes = new Set<number>();
|
||||
visibleRouteIds.forEach((otherRouteId) => {
|
||||
const stationIds =
|
||||
mapStore.routeStationsCache.get(otherRouteId) || [];
|
||||
stationIds.forEach((id: number) =>
|
||||
stationsInVisibleRoutes.add(id)
|
||||
);
|
||||
});
|
||||
|
||||
console.log(
|
||||
`[handleHideRoute] Found ${stationsInVisibleRoutes.size} unique stations in visible routes`
|
||||
);
|
||||
|
||||
// Скрываем станции, которые не используются в других ВИДИМЫХ маршрутах
|
||||
const stationsToHide = routeStationIds.filter(
|
||||
(id: number) => !stationsInVisibleRoutes.has(id)
|
||||
);
|
||||
|
||||
// Скрываем станции с карты
|
||||
stationsToHide.forEach((stationId: number) => {
|
||||
const pointFeature = mapService.pointSource.getFeatureById(
|
||||
`station-${stationId}`
|
||||
);
|
||||
if (pointFeature) {
|
||||
mapService.pointSource.removeFeature(
|
||||
pointFeature as Feature<Point>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Скрываем сам маршрут с карты
|
||||
const lineFeature = mapService.lineSource.getFeatureById(routeId);
|
||||
if (lineFeature) {
|
||||
mapService.lineSource.removeFeature(
|
||||
lineFeature as Feature<LineString>
|
||||
);
|
||||
}
|
||||
|
||||
// Добавляем в скрытые
|
||||
mapStore.hiddenRoutes.add(numericRouteId);
|
||||
saveHiddenRoutes(mapStore.hiddenRoutes); // Сохраняем в localStorage
|
||||
console.log(
|
||||
`[handleHideRoute] Added ${numericRouteId} to hiddenRoutes, stations to hide: ${stationsToHide.length}`
|
||||
);
|
||||
}
|
||||
|
||||
// Снимаем выделение
|
||||
mapService.unselect();
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"[handleHideRoute] Error toggling route visibility:",
|
||||
error
|
||||
);
|
||||
toast.error("Ошибка при изменении видимости маршрута");
|
||||
}
|
||||
},
|
||||
[mapService]
|
||||
);
|
||||
|
||||
const sortFeaturesByType = <T extends Feature<Geometry>>(
|
||||
features: T[],
|
||||
sortType: SortType
|
||||
@@ -2444,11 +2806,28 @@ const MapSightbar: React.FC<MapSightbarProps> = observer(
|
||||
const isSelected = selectedFeature?.getId() === fId;
|
||||
const isChecked = selectedIds.has(fId);
|
||||
|
||||
// Проверяем, скрыт ли маршрут
|
||||
const numericRouteId =
|
||||
featureType === "route"
|
||||
? parseInt(String(fId).split("-")[1], 10)
|
||||
: null;
|
||||
const isRouteHidden =
|
||||
numericRouteId !== null &&
|
||||
mapStore.hiddenRoutes.has(numericRouteId);
|
||||
|
||||
const description = feature.get("description") as
|
||||
| string
|
||||
| undefined;
|
||||
const showDescription =
|
||||
featureType === "station" &&
|
||||
description &&
|
||||
description.trim() !== "";
|
||||
|
||||
return (
|
||||
<div
|
||||
key={String(fId)}
|
||||
data-feature-id={fId}
|
||||
className={`flex items-start p-2 rounded-md group transition-colors duration-150 ${
|
||||
className={`flex items-start p-2 rounded-md group transition-colors duration-150 relative ${
|
||||
isSelected
|
||||
? "bg-orange-100 border border-orange-300"
|
||||
: "hover:bg-blue-50"
|
||||
@@ -2466,7 +2845,7 @@ const MapSightbar: React.FC<MapSightbarProps> = observer(
|
||||
</div>
|
||||
<div
|
||||
className="flex flex-col text-gray-800 text-sm flex-grow mr-2 min-w-0 cursor-pointer"
|
||||
onClick={() => handleFeatureClick(fId)}
|
||||
onClick={(e) => handleFeatureClick(fId, e)}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<IconComponent
|
||||
@@ -2491,6 +2870,11 @@ const MapSightbar: React.FC<MapSightbarProps> = observer(
|
||||
{fName}
|
||||
</span>
|
||||
</div>
|
||||
{showDescription && (
|
||||
<div className="mt-1 text-xs text-gray-600 line-clamp-2">
|
||||
{description}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-shrink-0 flex items-center space-x-1 opacity-60 group-hover:opacity-100">
|
||||
<button
|
||||
@@ -2505,6 +2889,37 @@ const MapSightbar: React.FC<MapSightbarProps> = observer(
|
||||
>
|
||||
<Pencil size={16} />
|
||||
</button>
|
||||
{featureType === "route" && (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
const routeId = parseInt(String(fId).split("-")[1], 10);
|
||||
navigate(`/route-preview/${routeId}`);
|
||||
}}
|
||||
className="p-1 rounded-full text-gray-400 hover:text-green-600 hover:bg-green-100 transition-colors"
|
||||
title="Предпросмотр маршрута"
|
||||
>
|
||||
<MapIcon size={16} />
|
||||
</button>
|
||||
)}
|
||||
{featureType === "route" && (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleHideRoute(fId);
|
||||
}}
|
||||
className={`p-1 rounded-full transition-colors ${
|
||||
isRouteHidden
|
||||
? "text-yellow-600 hover:text-yellow-700 hover:bg-yellow-100"
|
||||
: "text-gray-400 hover:text-yellow-600 hover:bg-yellow-100"
|
||||
}`}
|
||||
title={
|
||||
isRouteHidden ? "Показать на карте" : "Скрыть с карты"
|
||||
}
|
||||
>
|
||||
{isRouteHidden ? <Eye size={16} /> : <EyeOff size={16} />}
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
@@ -2528,6 +2943,17 @@ const MapSightbar: React.FC<MapSightbarProps> = observer(
|
||||
const toggleSection = (id: string) =>
|
||||
setActiveSection(activeSection === id ? null : id);
|
||||
|
||||
const [showSightsOptions, setShowSightsOptions] = useState(false);
|
||||
const sightsOptionsTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (sightsOptionsTimeoutRef.current) {
|
||||
clearTimeout(sightsOptionsTimeoutRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const sections = [
|
||||
{
|
||||
id: "layers",
|
||||
@@ -2567,20 +2993,60 @@ const MapSightbar: React.FC<MapSightbarProps> = observer(
|
||||
icon: <Landmark size={20} />,
|
||||
count: sortedSights.length,
|
||||
sortControl: (
|
||||
<div className="flex items-center space-x-2 p-3 bg-white border-b border-gray-200">
|
||||
<label className="text-sm text-gray-700">Сортировка:</label>
|
||||
<select
|
||||
value={sightSort}
|
||||
onChange={(e) => setSightSort(e.target.value as SortType)}
|
||||
className="border rounded px-2 py-1 text-sm"
|
||||
<div className="flex items-center justify-between gap-4 p-3 bg-white border-b border-gray-200">
|
||||
<div className="flex items-center space-x-2">
|
||||
<label className="text-sm text-gray-700">Сортировка:</label>
|
||||
<select
|
||||
value={sightSort}
|
||||
onChange={(e) => setSightSort(e.target.value as SortType)}
|
||||
className="border rounded px-2 py-1 text-sm"
|
||||
>
|
||||
<option value="name_asc">Имя ↑</option>
|
||||
<option value="name_desc">Имя ↓</option>
|
||||
<option value="created_asc">Дата создания ↑</option>
|
||||
<option value="created_desc">Дата создания ↓</option>
|
||||
<option value="updated_asc">Дата обновления ↑</option>
|
||||
<option value="updated_desc">Дата обновления ↓</option>
|
||||
</select>
|
||||
</div>
|
||||
<div
|
||||
className="relative"
|
||||
onMouseEnter={() => {
|
||||
sightsOptionsTimeoutRef.current = setTimeout(() => {
|
||||
setShowSightsOptions(true);
|
||||
}, 1000);
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
if (sightsOptionsTimeoutRef.current) {
|
||||
clearTimeout(sightsOptionsTimeoutRef.current);
|
||||
sightsOptionsTimeoutRef.current = null;
|
||||
}
|
||||
setShowSightsOptions(false);
|
||||
}}
|
||||
>
|
||||
<option value="name_asc">Имя ↑</option>
|
||||
<option value="name_desc">Имя ↓</option>
|
||||
<option value="created_asc">Дата создания ↑</option>
|
||||
<option value="created_desc">Дата создания ↓</option>
|
||||
<option value="updated_asc">Дата обновления ↑</option>
|
||||
<option value="updated_desc">Дата обновления ↓</option>
|
||||
</select>
|
||||
<button
|
||||
className={`px-2 py-1 rounded text-sm transition-colors ${
|
||||
mapStore.hideSightsByHiddenRoutes
|
||||
? "bg-blue-100 text-blue-700 hover:bg-blue-200"
|
||||
: "text-gray-600 hover:text-gray-800 hover:bg-gray-100"
|
||||
}`}
|
||||
onClick={() =>
|
||||
mapStore.setHideSightsByHiddenRoutes(
|
||||
!mapStore.hideSightsByHiddenRoutes
|
||||
)
|
||||
}
|
||||
>
|
||||
Скрыть
|
||||
</button>
|
||||
{showSightsOptions && (
|
||||
<div className="absolute right-0 mt-2 w-50 bg-white border border-gray-200 rounded-md shadow-md p-3 z-5000">
|
||||
<div className="text-xs text-gray-600">
|
||||
Будут скрыты все достопримечательности, привязанные к
|
||||
скрытым маршрутам.
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
content: renderFeatureList(sortedSights, "sight", Landmark),
|
||||
@@ -2887,6 +3353,27 @@ export const MapPage: React.FC = observer(() => {
|
||||
}
|
||||
}, [selectedCityId, mapServiceInstance, isDataLoading]);
|
||||
|
||||
// Перезагружаем данные при изменении настройки скрытия достопримечательностей
|
||||
useEffect(() => {
|
||||
if (mapServiceInstance && !isDataLoading) {
|
||||
// Очищаем текущие объекты на карте
|
||||
mapServiceInstance.pointSource.clear();
|
||||
mapServiceInstance.lineSource.clear();
|
||||
|
||||
// Загружаем новые данные с учетом фильтрации достопримечательностей
|
||||
mapServiceInstance.loadFeaturesFromApi(
|
||||
mapStore.stations,
|
||||
mapStore.routes,
|
||||
mapStore.sights
|
||||
);
|
||||
}
|
||||
}, [
|
||||
mapStore.hideSightsByHiddenRoutes,
|
||||
mapStore.hiddenRoutes.size,
|
||||
mapServiceInstance,
|
||||
isDataLoading,
|
||||
]);
|
||||
|
||||
const showLoader = isMapLoading || isDataLoading;
|
||||
const showContent = mapServiceInstance && !showLoader && !error;
|
||||
const isAnythingSelected =
|
||||
|
||||
@@ -562,7 +562,14 @@ const LinkedItemsContentsInner = <
|
||||
size="small"
|
||||
/>
|
||||
}
|
||||
label={String(item.name)}
|
||||
label={
|
||||
<div className="flex justify-between items-center w-full gap-10">
|
||||
<p>{String(item.name)}</p>
|
||||
<p className="text-xs text-gray-500 max-w-[300px] truncate text-ellipsis">
|
||||
{String(item.description)}
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
sx={{
|
||||
margin: 0,
|
||||
"& .MuiFormControlLabel-label": {
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
} from "@mui/material";
|
||||
import { MediaViewer } from "@widgets";
|
||||
import { MediaViewer, VideoPreviewCard } from "@widgets";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { ArrowLeft, Loader2, Save, Plus } from "lucide-react";
|
||||
import { useEffect, useState, useMemo } from "react";
|
||||
@@ -24,9 +24,10 @@ import { articlesStore } from "../../../shared/store/ArticlesStore";
|
||||
import { Route, routeStore } from "../../../shared/store/RouteStore";
|
||||
import {
|
||||
languageStore,
|
||||
SelectArticleModal,
|
||||
ArticleSelectOrCreateDialog,
|
||||
SelectMediaDialog,
|
||||
selectedCityStore,
|
||||
UploadMediaDialog,
|
||||
} from "@shared";
|
||||
|
||||
export const RouteCreatePage = observer(() => {
|
||||
@@ -37,8 +38,9 @@ export const RouteCreatePage = observer(() => {
|
||||
const [govRouteNumber, setGovRouteNumber] = useState("");
|
||||
const [governorAppeal, setGovernorAppeal] = useState<string>("");
|
||||
const [direction, setDirection] = useState("backward");
|
||||
const [scaleMin, setScaleMin] = useState("");
|
||||
const [scaleMax, setScaleMax] = useState("");
|
||||
const [scaleMin, setScaleMin] = useState("10");
|
||||
const [scaleMax, setScaleMax] = useState("100");
|
||||
const [routeName, setRouteName] = useState("");
|
||||
const [turn, setTurn] = useState("");
|
||||
const [centerLat, setCenterLat] = useState("");
|
||||
const [centerLng, setCenterLng] = useState("");
|
||||
@@ -48,6 +50,8 @@ export const RouteCreatePage = observer(() => {
|
||||
useState(false);
|
||||
const [isSelectVideoDialogOpen, setIsSelectVideoDialogOpen] = useState(false);
|
||||
const [isVideoPreviewOpen, setIsVideoPreviewOpen] = useState(false);
|
||||
const [isUploadVideoDialogOpen, setIsUploadVideoDialogOpen] = useState(false);
|
||||
const [fileToUpload, setFileToUpload] = useState<File | null>(null);
|
||||
const { language } = languageStore;
|
||||
|
||||
useEffect(() => {
|
||||
@@ -55,7 +59,6 @@ export const RouteCreatePage = observer(() => {
|
||||
articlesStore.getArticleList();
|
||||
}, [language]);
|
||||
|
||||
// Фильтруем перевозчиков только из выбранного города
|
||||
const filteredCarriers = useMemo(() => {
|
||||
const carriers =
|
||||
carrierStore.carriers[language as keyof typeof carrierStore.carriers]
|
||||
@@ -110,6 +113,8 @@ export const RouteCreatePage = observer(() => {
|
||||
const handleArticleSelect = (articleId: number) => {
|
||||
setGovernorAppeal(articleId.toString());
|
||||
setIsSelectArticleDialogOpen(false);
|
||||
// Обновляем список статей после создания новой
|
||||
articlesStore.getArticleList();
|
||||
};
|
||||
|
||||
const handleVideoSelect = (media: {
|
||||
@@ -122,6 +127,26 @@ export const RouteCreatePage = observer(() => {
|
||||
setIsSelectVideoDialogOpen(false);
|
||||
};
|
||||
|
||||
const handleVideoFileSelect = (file?: File) => {
|
||||
if (file) {
|
||||
setFileToUpload(file);
|
||||
setIsUploadVideoDialogOpen(true);
|
||||
} else {
|
||||
setIsSelectVideoDialogOpen(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleVideoUpload = (media: {
|
||||
id: string;
|
||||
filename: string;
|
||||
media_name?: string;
|
||||
media_type: number;
|
||||
}) => {
|
||||
setVideoPreview(media.id);
|
||||
setIsUploadVideoDialogOpen(false);
|
||||
setFileToUpload(null);
|
||||
};
|
||||
|
||||
const handleVideoPreviewClick = () => {
|
||||
setIsVideoPreviewOpen(true);
|
||||
};
|
||||
@@ -129,22 +154,75 @@ export const RouteCreatePage = observer(() => {
|
||||
const handleCreateRoute = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
// Преобразуем значения в нужные типы
|
||||
const carrier_id = Number(carrier);
|
||||
const governor_appeal = Number(governorAppeal);
|
||||
const scale_min = scaleMin ? Number(scaleMin) : undefined;
|
||||
const scale_max = scaleMax ? Number(scaleMax) : undefined;
|
||||
const rotate = turn ? Number(turn) : undefined;
|
||||
const center_latitude = centerLat ? Number(centerLat) : undefined;
|
||||
const center_longitude = centerLng ? Number(centerLng) : undefined;
|
||||
const route_direction = direction === "forward";
|
||||
|
||||
// Валидация обязательных полей
|
||||
if (!routeName.trim()) {
|
||||
toast.error("Заполните название маршрута");
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
if (!carrier) {
|
||||
toast.error("Выберите перевозчика");
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
if (!routeNumber.trim()) {
|
||||
toast.error("Заполните номер маршрута");
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
if (!govRouteNumber.trim()) {
|
||||
toast.error("Заполните номер маршрута в Говорящем Городе");
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
if (!governorAppeal) {
|
||||
toast.error("Выберите статью для обращения к пассажирам");
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const validationResult = validateCoordinates(routeCoords);
|
||||
if (validationResult !== true) {
|
||||
toast.error(validationResult);
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Валидация масштабов
|
||||
const scale_min = scaleMin ? Number(scaleMin) : null;
|
||||
const scale_max = scaleMax ? Number(scaleMax) : null;
|
||||
console.log(scale_min, scale_max);
|
||||
if (
|
||||
scale_min === 0 ||
|
||||
scale_max === 0 ||
|
||||
scale_min === null ||
|
||||
scale_max === null
|
||||
) {
|
||||
toast.error("Масштабы не могут быть равны 0");
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
scale_min !== null &&
|
||||
scale_max !== null &&
|
||||
scale_max !== undefined &&
|
||||
scale_min > scale_max
|
||||
) {
|
||||
toast.error("Максимальный масштаб не может быть меньше минимального");
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Преобразуем значения в нужные типы
|
||||
const carrier_id = Number(carrier);
|
||||
const governor_appeal = Number(governorAppeal);
|
||||
const rotate = turn ? Number(turn) : undefined;
|
||||
const center_latitude = centerLat ? Number(centerLat) : undefined;
|
||||
const center_longitude = centerLng ? Number(centerLng) : undefined;
|
||||
const route_direction = direction === "forward";
|
||||
|
||||
// Координаты маршрута как массив массивов чисел
|
||||
const path = routeCoords
|
||||
.trim()
|
||||
@@ -167,9 +245,10 @@ export const RouteCreatePage = observer(() => {
|
||||
route_number: routeNumber,
|
||||
route_sys_number: govRouteNumber,
|
||||
governor_appeal,
|
||||
route_name: routeName,
|
||||
route_direction,
|
||||
scale_min,
|
||||
scale_max,
|
||||
scale_min: scale_min !== null ? scale_min : 0,
|
||||
scale_max: scale_max !== null ? scale_max : 0,
|
||||
rotate,
|
||||
center_latitude,
|
||||
center_longitude,
|
||||
@@ -208,6 +287,13 @@ export const RouteCreatePage = observer(() => {
|
||||
|
||||
<div className="flex flex-col gap-10 w-full items-end">
|
||||
<Box className="flex flex-col gap-6 w-full">
|
||||
<TextField
|
||||
className="w-full"
|
||||
label="Название маршрута"
|
||||
required
|
||||
value={routeName}
|
||||
onChange={(e) => setRouteName(e.target.value)}
|
||||
/>
|
||||
<FormControl fullWidth required>
|
||||
<InputLabel>Выберите перевозчика</InputLabel>
|
||||
<Select
|
||||
@@ -247,7 +333,6 @@ export const RouteCreatePage = observer(() => {
|
||||
const lines = routeCoords.split("\n");
|
||||
const lastLine = lines[lines.length - 1];
|
||||
|
||||
// Если мы на последней строке и она не пустая
|
||||
if (lastLine && lastLine.trim()) {
|
||||
e.preventDefault();
|
||||
const newValue = routeCoords + "\n";
|
||||
@@ -279,6 +364,7 @@ export const RouteCreatePage = observer(() => {
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
className="w-full"
|
||||
label="Номер маршрута в Говорящем Городе"
|
||||
@@ -287,99 +373,42 @@ export const RouteCreatePage = observer(() => {
|
||||
onChange={(e) => setGovRouteNumber(e.target.value)}
|
||||
/>
|
||||
|
||||
{/* Заменяем Select на кнопку для выбора статьи */}
|
||||
<Box className="flex flex-col gap-2">
|
||||
<label className="text-sm font-medium text-gray-700">
|
||||
Обращение к пассажирам
|
||||
</label>
|
||||
<Box className="flex gap-2">
|
||||
<TextField
|
||||
className="flex-1"
|
||||
value={selectedArticle?.heading || "Статья не выбрана"}
|
||||
placeholder="Выберите статью"
|
||||
disabled
|
||||
sx={{
|
||||
"& .MuiInputBase-input": {
|
||||
color: selectedArticle ? "inherit" : "#999",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={() => setIsSelectArticleDialogOpen(true)}
|
||||
startIcon={<Plus size={16} />}
|
||||
sx={{ minWidth: "auto", px: 2 }}
|
||||
>
|
||||
Выбрать
|
||||
</Button>
|
||||
</Box>
|
||||
<Typography variant="subtitle1" sx={{ fontWeight: 600 }}>
|
||||
Обращение к пассажирам
|
||||
</Typography>
|
||||
<Box className="flex gap-2">
|
||||
<TextField
|
||||
className="flex-1"
|
||||
value={selectedArticle?.heading || "Статья не выбрана"}
|
||||
placeholder="Выберите статью"
|
||||
disabled
|
||||
fullWidth
|
||||
sx={{
|
||||
"& .MuiInputBase-input": {
|
||||
color: selectedArticle ? "inherit" : "#999",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={() => setIsSelectArticleDialogOpen(true)}
|
||||
startIcon={<Plus size={16} />}
|
||||
sx={{ minWidth: "auto", px: 2 }}
|
||||
>
|
||||
Выбрать
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
{/* Селектор видеозаставки */}
|
||||
<Box className="flex flex-col gap-2">
|
||||
<label className="text-sm font-medium text-gray-700">
|
||||
Видеозаставка
|
||||
</label>
|
||||
<Box className="flex gap-2">
|
||||
<Box
|
||||
className="flex-1"
|
||||
onClick={handleVideoPreviewClick}
|
||||
sx={{
|
||||
cursor:
|
||||
videoPreview && videoPreview !== "" ? "pointer" : "default",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
className="w-full h-[50px] border border-gray-400 rounded-sm flex items-center justify-between px-4"
|
||||
sx={{
|
||||
"& .MuiInputBase-input": {
|
||||
color:
|
||||
videoPreview && videoPreview !== ""
|
||||
? "inherit"
|
||||
: "#999",
|
||||
cursor:
|
||||
videoPreview && videoPreview !== ""
|
||||
? "pointer"
|
||||
: "default",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Typography variant="body1" className="text-sm">
|
||||
{videoPreview && videoPreview !== ""
|
||||
? "Видео выбрано"
|
||||
: "Видео не выбрано"}
|
||||
</Typography>
|
||||
{videoPreview && videoPreview !== "" && (
|
||||
<Box
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setVideoPreview("");
|
||||
}}
|
||||
sx={{
|
||||
cursor: "pointer",
|
||||
color: "#999",
|
||||
"&:hover": {
|
||||
color: "#666",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Typography variant="body1" className="text-lg font-bold">
|
||||
×
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={() => setIsSelectVideoDialogOpen(true)}
|
||||
startIcon={<Plus size={16} />}
|
||||
sx={{ minWidth: "auto", px: 2 }}
|
||||
>
|
||||
Выбрать
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
<VideoPreviewCard
|
||||
title="Видеозаставка"
|
||||
videoId={videoPreview}
|
||||
onVideoClick={handleVideoPreviewClick}
|
||||
onDeleteVideoClick={() => {
|
||||
setVideoPreview("");
|
||||
}}
|
||||
onSelectVideoClick={handleVideoFileSelect}
|
||||
className="w-full"
|
||||
/>
|
||||
|
||||
<FormControl fullWidth required>
|
||||
<InputLabel>Прямой/обратный маршрут</InputLabel>
|
||||
@@ -395,15 +424,42 @@ export const RouteCreatePage = observer(() => {
|
||||
<TextField
|
||||
className="w-full"
|
||||
label="Масштаб (мин)"
|
||||
type="number"
|
||||
value={scaleMin}
|
||||
onChange={(e) => setScaleMin(e.target.value)}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
setScaleMin(value);
|
||||
// Если максимальный масштаб стал меньше минимального, обновляем его
|
||||
if (value && scaleMax && Number(value) > Number(scaleMax)) {
|
||||
setScaleMax(value);
|
||||
}
|
||||
}}
|
||||
error={
|
||||
scaleMin !== "" &&
|
||||
scaleMax !== "" &&
|
||||
Number(scaleMin) > Number(scaleMax)
|
||||
}
|
||||
required
|
||||
helperText={
|
||||
scaleMin !== "" &&
|
||||
scaleMax !== "" &&
|
||||
Number(scaleMin) > Number(scaleMax)
|
||||
? "Минимальный масштаб не может быть больше максимального"
|
||||
: ""
|
||||
}
|
||||
/>
|
||||
<TextField
|
||||
className="w-full"
|
||||
label="Масштаб (макс)"
|
||||
type="number"
|
||||
value={scaleMax}
|
||||
onChange={(e) => setScaleMax(e.target.value)}
|
||||
required
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
setScaleMax(value);
|
||||
}}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
className="w-full"
|
||||
label="Поворот"
|
||||
@@ -440,23 +496,17 @@ export const RouteCreatePage = observer(() => {
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Модальное окно выбора статьи */}
|
||||
<SelectArticleModal
|
||||
<ArticleSelectOrCreateDialog
|
||||
open={isSelectArticleDialogOpen}
|
||||
onClose={() => setIsSelectArticleDialogOpen(false)}
|
||||
onSelectArticle={handleArticleSelect}
|
||||
/>
|
||||
|
||||
{/* Модальное окно выбора видео */}
|
||||
<SelectMediaDialog
|
||||
open={isSelectVideoDialogOpen}
|
||||
onClose={() => setIsSelectVideoDialogOpen(false)}
|
||||
onSelectMedia={handleVideoSelect}
|
||||
mediaType={2}
|
||||
/>
|
||||
|
||||
{/* Модальное окно предпросмотра видео */}
|
||||
{videoPreview && videoPreview !== "" && (
|
||||
<Dialog
|
||||
open={isVideoPreviewOpen}
|
||||
@@ -483,6 +533,18 @@ export const RouteCreatePage = observer(() => {
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
)}
|
||||
<UploadMediaDialog
|
||||
open={isUploadVideoDialogOpen}
|
||||
onClose={() => {
|
||||
setIsUploadVideoDialogOpen(false);
|
||||
setFileToUpload(null);
|
||||
}}
|
||||
hardcodeType="video_preview"
|
||||
contextObjectName={routeName || "Маршрут"}
|
||||
contextType="sight"
|
||||
initialFile={fileToUpload || undefined}
|
||||
afterUpload={handleVideoUpload}
|
||||
/>
|
||||
</Paper>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
} from "@mui/material";
|
||||
import { MediaViewer } from "@widgets";
|
||||
import { MediaViewer, VideoPreviewCard } from "@widgets";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { ArrowLeft, Copy, Save, Plus } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
@@ -24,8 +24,9 @@ import { articlesStore } from "../../../shared/store/ArticlesStore";
|
||||
import {
|
||||
routeStore,
|
||||
languageStore,
|
||||
SelectArticleModal,
|
||||
ArticleSelectOrCreateDialog,
|
||||
SelectMediaDialog,
|
||||
UploadMediaDialog,
|
||||
} from "@shared";
|
||||
import { toast } from "react-toastify";
|
||||
import { stationsStore } from "@shared";
|
||||
@@ -40,12 +41,13 @@ export const RouteEditPage = observer(() => {
|
||||
useState(false);
|
||||
const [isSelectVideoDialogOpen, setIsSelectVideoDialogOpen] = useState(false);
|
||||
const [isVideoPreviewOpen, setIsVideoPreviewOpen] = useState(false);
|
||||
const [isUploadVideoDialogOpen, setIsUploadVideoDialogOpen] = useState(false);
|
||||
const [fileToUpload, setFileToUpload] = useState<File | null>(null);
|
||||
const { language } = languageStore;
|
||||
const [coordinates, setCoordinates] = useState<string>("");
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
// Устанавливаем русский язык при загрузке страницы
|
||||
const response = await routeStore.getRoute(Number(id));
|
||||
routeStore.setEditRouteData(response);
|
||||
languageStore.setLanguage("ru");
|
||||
@@ -72,10 +74,67 @@ export const RouteEditPage = observer(() => {
|
||||
}, [editRouteData.path]);
|
||||
|
||||
const handleSave = async () => {
|
||||
// Валидация обязательных полей
|
||||
if (!editRouteData.route_name?.trim()) {
|
||||
toast.error("Заполните название маршрута");
|
||||
return;
|
||||
}
|
||||
if (!editRouteData.carrier_id) {
|
||||
toast.error("Выберите перевозчика");
|
||||
return;
|
||||
}
|
||||
if (!editRouteData.route_number?.trim()) {
|
||||
toast.error("Заполните номер маршрута");
|
||||
return;
|
||||
}
|
||||
if (!editRouteData.route_sys_number?.trim()) {
|
||||
toast.error("Заполните номер маршрута в Говорящем Городе");
|
||||
return;
|
||||
}
|
||||
if (!editRouteData.governor_appeal) {
|
||||
toast.error("Выберите статью для обращения к пассажирам");
|
||||
return;
|
||||
}
|
||||
|
||||
const validationResult = validateCoordinates(coordinates);
|
||||
if (validationResult !== true) {
|
||||
toast.error(validationResult);
|
||||
return;
|
||||
}
|
||||
|
||||
// Валидация масштабов
|
||||
if (
|
||||
editRouteData.scale_min !== null &&
|
||||
editRouteData.scale_min !== undefined &&
|
||||
editRouteData.scale_max !== null &&
|
||||
editRouteData.scale_max !== undefined &&
|
||||
editRouteData.scale_min > editRouteData.scale_max
|
||||
) {
|
||||
toast.error("Максимальный масштаб не может быть меньше минимального");
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
editRouteData.scale_min === 0 ||
|
||||
editRouteData.scale_max === 0 ||
|
||||
editRouteData.scale_min === null ||
|
||||
editRouteData.scale_max === null
|
||||
) {
|
||||
toast.error("Масштабы не могут быть равны 0");
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
await routeStore.editRoute(Number(id));
|
||||
toast.success("Маршрут успешно сохранен");
|
||||
setIsLoading(false);
|
||||
try {
|
||||
await routeStore.editRoute(Number(id));
|
||||
toast.success("Маршрут успешно сохранен");
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
toast.error("Произошла ошибка при сохранении маршрута");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const validateCoordinates = (value: string) => {
|
||||
@@ -125,6 +184,8 @@ export const RouteEditPage = observer(() => {
|
||||
governor_appeal: articleId,
|
||||
});
|
||||
setIsSelectArticleDialogOpen(false);
|
||||
// Обновляем список статей после создания новой
|
||||
articlesStore.getArticleList();
|
||||
};
|
||||
|
||||
const handleVideoSelect = (media: {
|
||||
@@ -139,6 +200,28 @@ export const RouteEditPage = observer(() => {
|
||||
setIsSelectVideoDialogOpen(false);
|
||||
};
|
||||
|
||||
const handleVideoFileSelect = (file?: File) => {
|
||||
if (file) {
|
||||
setFileToUpload(file);
|
||||
setIsUploadVideoDialogOpen(true);
|
||||
} else {
|
||||
setIsSelectVideoDialogOpen(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleVideoUpload = (media: {
|
||||
id: string;
|
||||
filename: string;
|
||||
media_name?: string;
|
||||
media_type: number;
|
||||
}) => {
|
||||
routeStore.setEditRouteData({
|
||||
video_preview: media.id,
|
||||
});
|
||||
setIsUploadVideoDialogOpen(false);
|
||||
setFileToUpload(null);
|
||||
};
|
||||
|
||||
const handleVideoPreviewClick = () => {
|
||||
if (editRouteData.video_preview && editRouteData.video_preview !== "") {
|
||||
setIsVideoPreviewOpen(true);
|
||||
@@ -164,6 +247,17 @@ export const RouteEditPage = observer(() => {
|
||||
|
||||
<div className="flex flex-col gap-10 w-full items-end">
|
||||
<Box className="flex flex-col gap-6 w-full">
|
||||
<TextField
|
||||
className="w-full"
|
||||
label="Название маршрута"
|
||||
required
|
||||
value={editRouteData.route_name || ""}
|
||||
onChange={(e) =>
|
||||
routeStore.setEditRouteData({
|
||||
route_name: e.target.value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
<FormControl fullWidth required>
|
||||
<InputLabel>Выберите перевозчика</InputLabel>
|
||||
<Select
|
||||
@@ -235,7 +329,6 @@ export const RouteEditPage = observer(() => {
|
||||
const lines = coordinates.split("\n");
|
||||
const lastLine = lines[lines.length - 1];
|
||||
|
||||
// Если мы на последней строке и она не пустая
|
||||
if (lastLine && lastLine.trim()) {
|
||||
e.preventDefault();
|
||||
const newValue = coordinates + "\n";
|
||||
@@ -279,110 +372,6 @@ export const RouteEditPage = observer(() => {
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Заменяем Select на кнопку для выбора статьи */}
|
||||
<Box className="flex flex-col gap-2">
|
||||
<label className="text-sm font-medium text-gray-700">
|
||||
Обращение к пассажирам
|
||||
</label>
|
||||
<Box className="flex gap-2">
|
||||
<TextField
|
||||
className="flex-1"
|
||||
value={selectedArticle?.heading || "Статья не выбрана"}
|
||||
placeholder="Выберите статью"
|
||||
disabled
|
||||
sx={{
|
||||
"& .MuiInputBase-input": {
|
||||
color: selectedArticle ? "inherit" : "#999",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={() => setIsSelectArticleDialogOpen(true)}
|
||||
startIcon={<Plus size={16} />}
|
||||
sx={{ minWidth: "auto", px: 2 }}
|
||||
>
|
||||
Выбрать
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Селектор видеозаставки */}
|
||||
<Box className="flex flex-col gap-2">
|
||||
<label className="text-sm font-medium text-gray-700">
|
||||
Видеозаставка
|
||||
</label>
|
||||
<Box className="flex gap-2">
|
||||
<Box
|
||||
className="flex-1"
|
||||
onClick={handleVideoPreviewClick}
|
||||
sx={{
|
||||
cursor:
|
||||
editRouteData.video_preview &&
|
||||
editRouteData.video_preview !== ""
|
||||
? "pointer"
|
||||
: "default",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
className="w-full h-[50px] border border-gray-400 rounded-sm flex items-center justify-between px-4"
|
||||
sx={{
|
||||
"& .MuiInputBase-input": {
|
||||
color:
|
||||
editRouteData.video_preview &&
|
||||
editRouteData.video_preview !== ""
|
||||
? "inherit"
|
||||
: "#999",
|
||||
cursor:
|
||||
editRouteData.video_preview &&
|
||||
editRouteData.video_preview !== ""
|
||||
? "pointer"
|
||||
: "default",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Typography variant="body1" className="text-sm">
|
||||
{editRouteData.video_preview &&
|
||||
editRouteData.video_preview !== ""
|
||||
? "Видео выбрано"
|
||||
: "Видео не выбрано"}
|
||||
</Typography>
|
||||
{editRouteData.video_preview &&
|
||||
editRouteData.video_preview !== "" && (
|
||||
<Box
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
routeStore.setEditRouteData({ video_preview: "" });
|
||||
}}
|
||||
sx={{
|
||||
cursor: "pointer",
|
||||
color: "#999",
|
||||
"&:hover": {
|
||||
color: "#666",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="body1"
|
||||
className="text-lg font-bold"
|
||||
>
|
||||
×
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={() => setIsSelectVideoDialogOpen(true)}
|
||||
startIcon={<Plus size={16} />}
|
||||
sx={{ minWidth: "auto", px: 2 }}
|
||||
>
|
||||
Выбрать
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<FormControl fullWidth required>
|
||||
<InputLabel>Прямой/обратный маршрут</InputLabel>
|
||||
<Select
|
||||
@@ -401,17 +390,33 @@ export const RouteEditPage = observer(() => {
|
||||
<TextField
|
||||
className="w-full"
|
||||
label="Масштаб (мин)"
|
||||
type="number"
|
||||
value={editRouteData.scale_min ?? ""}
|
||||
onChange={(e) =>
|
||||
onChange={(e) => {
|
||||
const value =
|
||||
e.target.value === "" ? null : parseFloat(e.target.value);
|
||||
routeStore.setEditRouteData({
|
||||
scale_min:
|
||||
e.target.value === "" ? null : parseFloat(e.target.value),
|
||||
})
|
||||
}
|
||||
scale_min: value,
|
||||
});
|
||||
// Если максимальный масштаб стал меньше минимального, обновляем его
|
||||
if (
|
||||
value !== null &&
|
||||
editRouteData.scale_max !== null &&
|
||||
editRouteData.scale_max !== undefined &&
|
||||
value > editRouteData.scale_max
|
||||
) {
|
||||
routeStore.setEditRouteData({
|
||||
scale_max: value,
|
||||
});
|
||||
}
|
||||
}}
|
||||
required
|
||||
/>
|
||||
<TextField
|
||||
className="w-full"
|
||||
required
|
||||
label="Масштаб (макс)"
|
||||
type="number"
|
||||
value={editRouteData.scale_max ?? ""}
|
||||
onChange={(e) =>
|
||||
routeStore.setEditRouteData({
|
||||
@@ -419,6 +424,22 @@ export const RouteEditPage = observer(() => {
|
||||
e.target.value === "" ? null : parseFloat(e.target.value),
|
||||
})
|
||||
}
|
||||
error={
|
||||
editRouteData.scale_min !== null &&
|
||||
editRouteData.scale_min !== undefined &&
|
||||
editRouteData.scale_max !== null &&
|
||||
editRouteData.scale_max !== undefined &&
|
||||
editRouteData.scale_max < editRouteData.scale_min
|
||||
}
|
||||
helperText={
|
||||
editRouteData.scale_min !== null &&
|
||||
editRouteData.scale_min !== undefined &&
|
||||
editRouteData.scale_max !== null &&
|
||||
editRouteData.scale_max !== undefined &&
|
||||
editRouteData.scale_max < editRouteData.scale_min
|
||||
? "Максимальный масштаб не может быть меньше минимального"
|
||||
: ""
|
||||
}
|
||||
/>
|
||||
<TextField
|
||||
className="w-full"
|
||||
@@ -453,6 +474,43 @@ export const RouteEditPage = observer(() => {
|
||||
})
|
||||
}
|
||||
/>
|
||||
|
||||
<Typography variant="subtitle1" sx={{ fontWeight: 600 }}>
|
||||
Обращение к пассажирам
|
||||
</Typography>
|
||||
<Box className="flex gap-2">
|
||||
<TextField
|
||||
className="flex-1"
|
||||
value={selectedArticle?.heading || "Статья не выбрана"}
|
||||
placeholder="Выберите статью"
|
||||
disabled
|
||||
fullWidth
|
||||
sx={{
|
||||
"& .MuiInputBase-input": {
|
||||
color: selectedArticle ? "inherit" : "#999",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={() => setIsSelectArticleDialogOpen(true)}
|
||||
startIcon={<Plus size={16} />}
|
||||
sx={{ minWidth: "auto", px: 2 }}
|
||||
>
|
||||
Выбрать
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
<VideoPreviewCard
|
||||
title="Видеозаставка"
|
||||
videoId={editRouteData.video_preview}
|
||||
onVideoClick={handleVideoPreviewClick}
|
||||
onDeleteVideoClick={() => {
|
||||
routeStore.setEditRouteData({ video_preview: "" });
|
||||
}}
|
||||
onSelectVideoClick={handleVideoFileSelect}
|
||||
className="w-full"
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<LinkedItems
|
||||
@@ -493,23 +551,17 @@ export const RouteEditPage = observer(() => {
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Модальное окно выбора статьи */}
|
||||
<SelectArticleModal
|
||||
<ArticleSelectOrCreateDialog
|
||||
open={isSelectArticleDialogOpen}
|
||||
onClose={() => setIsSelectArticleDialogOpen(false)}
|
||||
onSelectArticle={handleArticleSelect}
|
||||
/>
|
||||
|
||||
{/* Модальное окно выбора видео */}
|
||||
<SelectMediaDialog
|
||||
open={isSelectVideoDialogOpen}
|
||||
onClose={() => setIsSelectVideoDialogOpen(false)}
|
||||
onSelectMedia={handleVideoSelect}
|
||||
mediaType={2}
|
||||
/>
|
||||
|
||||
{/* Модальное окно предпросмотра видео */}
|
||||
<Dialog
|
||||
open={isVideoPreviewOpen}
|
||||
onClose={() => setIsVideoPreviewOpen(false)}
|
||||
@@ -519,19 +571,33 @@ export const RouteEditPage = observer(() => {
|
||||
<DialogTitle>Предпросмотр видео</DialogTitle>
|
||||
<DialogContent>
|
||||
<Box className="flex justify-center items-center p-4">
|
||||
<MediaViewer
|
||||
media={{
|
||||
id: editRouteData.video_preview,
|
||||
media_type: 2,
|
||||
filename: "video_preview",
|
||||
}}
|
||||
/>
|
||||
{editRouteData.video_preview && (
|
||||
<MediaViewer
|
||||
media={{
|
||||
id: editRouteData.video_preview,
|
||||
media_type: 2,
|
||||
filename: "video_preview",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setIsVideoPreviewOpen(false)}>Закрыть</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
<UploadMediaDialog
|
||||
open={isUploadVideoDialogOpen}
|
||||
onClose={() => {
|
||||
setIsUploadVideoDialogOpen(false);
|
||||
setFileToUpload(null);
|
||||
}}
|
||||
hardcodeType="video_preview"
|
||||
contextObjectName={editRouteData.route_name || "Маршрут"}
|
||||
contextType="sight"
|
||||
initialFile={fileToUpload || undefined}
|
||||
afterUpload={handleVideoUpload}
|
||||
/>
|
||||
</Paper>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -50,6 +50,22 @@ export const RouteListPage = observer(() => {
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
field: "route_name",
|
||||
headerName: "Название маршрута",
|
||||
flex: 1,
|
||||
renderCell: (params: GridRenderCellParams) => {
|
||||
return (
|
||||
<div className="w-full h-full flex items-center">
|
||||
{params.value ? (
|
||||
params.value
|
||||
) : (
|
||||
<Minus size={20} className="text-red-500" />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
field: "route_number",
|
||||
headerName: "Номер маршрута",
|
||||
@@ -100,9 +116,7 @@ export const RouteListPage = observer(() => {
|
||||
<button onClick={() => navigate(`/route-preview/${params.row.id}`)}>
|
||||
<Map size={20} className="text-purple-500" />
|
||||
</button>
|
||||
{/* <button onClick={() => navigate(`/route/${params.row.id}`)}>
|
||||
<Eye size={20} className="text-green-500" />
|
||||
</button> */}
|
||||
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsDeleteModalOpen(true);
|
||||
@@ -122,6 +136,7 @@ export const RouteListPage = observer(() => {
|
||||
carrier_id: route.carrier_id,
|
||||
route_number: route.route_number,
|
||||
route_direction: route.route_direction ? "Прямой" : "Обратный",
|
||||
route_name: route.route_name,
|
||||
}));
|
||||
|
||||
return (
|
||||
|
||||
@@ -118,7 +118,7 @@ export function RightSidebar() {
|
||||
borderRadius={2}
|
||||
>
|
||||
<Typography variant="h6" sx={{ mb: 2, color: "#fff" }} textAlign="center">
|
||||
Детали о достопримечательностях
|
||||
Настройка маршрута
|
||||
</Typography>
|
||||
|
||||
<Stack spacing={2} direction="row" alignItems="center">
|
||||
|
||||
@@ -79,7 +79,7 @@ export const Sight = ({ sight, id }: Readonly<SightProps>) => {
|
||||
|
||||
const [texture, setTexture] = useState(Texture.EMPTY);
|
||||
useEffect(() => {
|
||||
Assets.load("/SightIcon.png").then(setTexture);
|
||||
Assets.load("/sight_icon.svg").then(setTexture);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {}, [id, sight.latitude, sight.longitude]);
|
||||
|
||||
1792
src/pages/Route/route-preview/web-gl/web-gl-version.tsx
Normal file
321
src/pages/Sight/LinkedStations.tsx
Normal file
@@ -0,0 +1,321 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import {
|
||||
Stack,
|
||||
Typography,
|
||||
Button,
|
||||
Accordion,
|
||||
AccordionSummary,
|
||||
AccordionDetails,
|
||||
useTheme,
|
||||
TextField,
|
||||
Autocomplete,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
Table,
|
||||
TableHead,
|
||||
TableRow,
|
||||
Paper,
|
||||
TableBody,
|
||||
} from "@mui/material";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
|
||||
|
||||
import { authInstance, languageStore, selectedCityStore } from "@shared";
|
||||
|
||||
type Field<T> = {
|
||||
label: string;
|
||||
data: keyof T;
|
||||
render?: (value: any) => React.ReactNode;
|
||||
};
|
||||
|
||||
type LinkedStationsProps<T> = {
|
||||
parentId: string | number;
|
||||
fields: Field<T>[];
|
||||
setItemsParent?: (items: T[]) => void;
|
||||
type: "show" | "edit";
|
||||
onUpdate?: () => void;
|
||||
disableCreation?: boolean;
|
||||
updatedLinkedItems?: T[];
|
||||
refresh?: number;
|
||||
};
|
||||
|
||||
export const LinkedStations = <
|
||||
T extends { id: number; name: string; [key: string]: any }
|
||||
>(
|
||||
props: LinkedStationsProps<T>
|
||||
) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Accordion sx={{ width: "100%" }}>
|
||||
<AccordionSummary
|
||||
expandIcon={<ExpandMoreIcon />}
|
||||
sx={{
|
||||
background: theme.palette.background.paper,
|
||||
borderBottom: `1px solid ${theme.palette.divider}`,
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<Typography variant="subtitle1" fontWeight="bold">
|
||||
Привязанные остановки
|
||||
</Typography>
|
||||
</AccordionSummary>
|
||||
|
||||
<AccordionDetails
|
||||
sx={{ background: theme.palette.background.paper, width: "100%" }}
|
||||
>
|
||||
<Stack gap={2} width="100%">
|
||||
<LinkedStationsContents {...props} />
|
||||
</Stack>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const LinkedStationsContentsInner = <
|
||||
T extends { id: number; name: string; [key: string]: any }
|
||||
>({
|
||||
parentId,
|
||||
setItemsParent,
|
||||
fields,
|
||||
type,
|
||||
onUpdate,
|
||||
disableCreation = false,
|
||||
updatedLinkedItems,
|
||||
refresh,
|
||||
}: LinkedStationsProps<T>) => {
|
||||
const { language } = languageStore;
|
||||
|
||||
const [allItems, setAllItems] = useState<T[]>([]);
|
||||
const [linkedItems, setLinkedItems] = useState<T[]>([]);
|
||||
const [selectedItemId, setSelectedItemId] = useState<number | null>(null);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {}, [error]);
|
||||
|
||||
const parentResource = "sight";
|
||||
const childResource = "station";
|
||||
|
||||
const availableItems = allItems
|
||||
.filter((item) => !linkedItems.some((linked) => linked.id === item.id))
|
||||
.filter((item) => {
|
||||
const selectedCityId = selectedCityStore.selectedCityId;
|
||||
if (selectedCityId && "city_id" in item) {
|
||||
return item.city_id === selectedCityId;
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
useEffect(() => {
|
||||
if (updatedLinkedItems) {
|
||||
setLinkedItems(updatedLinkedItems);
|
||||
}
|
||||
}, [updatedLinkedItems]);
|
||||
|
||||
useEffect(() => {
|
||||
setItemsParent?.(linkedItems);
|
||||
}, [linkedItems, setItemsParent]);
|
||||
|
||||
const linkItem = () => {
|
||||
if (selectedItemId !== null) {
|
||||
setError(null);
|
||||
const requestData = {
|
||||
station_id: selectedItemId,
|
||||
};
|
||||
|
||||
authInstance
|
||||
.post(`/${parentResource}/${parentId}/${childResource}`, requestData)
|
||||
.then(() => {
|
||||
const newItem = allItems.find((item) => item.id === selectedItemId);
|
||||
if (newItem) {
|
||||
setLinkedItems([...linkedItems, newItem]);
|
||||
}
|
||||
setSelectedItemId(null);
|
||||
onUpdate?.();
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error linking station:", error);
|
||||
setError("Failed to link station");
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const deleteItem = (itemId: number) => {
|
||||
setError(null);
|
||||
authInstance
|
||||
.delete(`/${parentResource}/${parentId}/${childResource}`, {
|
||||
data: { [`${childResource}_id`]: itemId },
|
||||
})
|
||||
.then(() => {
|
||||
setLinkedItems(linkedItems.filter((item) => item.id !== itemId));
|
||||
onUpdate?.();
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error deleting station:", error);
|
||||
setError("Failed to delete station");
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (parentId) {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
authInstance
|
||||
.get(`/${parentResource}/${parentId}/${childResource}`)
|
||||
.then((response) => {
|
||||
setLinkedItems(response?.data || []);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error fetching linked stations:", error);
|
||||
setError("Failed to load linked stations");
|
||||
setLinkedItems([]);
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLoading(false);
|
||||
});
|
||||
}
|
||||
}, [parentId, language, refresh]);
|
||||
|
||||
useEffect(() => {
|
||||
if (type === "edit") {
|
||||
setError(null);
|
||||
authInstance
|
||||
.get(`/${childResource}`)
|
||||
.then((response) => {
|
||||
setAllItems(response?.data || []);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error fetching all stations:", error);
|
||||
setError("Failed to load available stations");
|
||||
setAllItems([]);
|
||||
});
|
||||
}
|
||||
}, [type]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{linkedItems?.length > 0 && (
|
||||
<TableContainer component={Paper} sx={{ width: "100%" }}>
|
||||
<Table sx={{ width: "100%" }}>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell key="id" width="60px">
|
||||
№
|
||||
</TableCell>
|
||||
{fields.map((field) => (
|
||||
<TableCell key={String(field.data)}>{field.label}</TableCell>
|
||||
))}
|
||||
{type === "edit" && (
|
||||
<TableCell width="120px">Действие</TableCell>
|
||||
)}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
|
||||
<TableBody>
|
||||
{linkedItems.map((item, index) => (
|
||||
<TableRow key={item.id} hover>
|
||||
<TableCell>{index + 1}</TableCell>
|
||||
{fields.map((field, idx) => (
|
||||
<TableCell key={String(field.data) + String(idx)}>
|
||||
{field.render
|
||||
? field.render(item[field.data])
|
||||
: item[field.data]}
|
||||
</TableCell>
|
||||
))}
|
||||
{type === "edit" && (
|
||||
<TableCell>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="error"
|
||||
size="small"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
deleteItem(item.id);
|
||||
}}
|
||||
>
|
||||
Отвязать
|
||||
</Button>
|
||||
</TableCell>
|
||||
)}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
)}
|
||||
|
||||
{linkedItems.length === 0 && !isLoading && (
|
||||
<Typography color="textSecondary" textAlign="center" py={2}>
|
||||
Остановки не найдены
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
{type === "edit" && !disableCreation && (
|
||||
<Stack gap={2} mt={2}>
|
||||
<Typography variant="subtitle1">Добавить остановку</Typography>
|
||||
<Autocomplete
|
||||
fullWidth
|
||||
value={
|
||||
availableItems?.find((item) => item.id === selectedItemId) || null
|
||||
}
|
||||
onChange={(_, newValue) => setSelectedItemId(newValue?.id || null)}
|
||||
options={availableItems}
|
||||
getOptionLabel={(item) => String(item.name)}
|
||||
renderInput={(params) => (
|
||||
<TextField {...params} label="Выберите остановку" fullWidth />
|
||||
)}
|
||||
isOptionEqualToValue={(option, value) => option.id === value?.id}
|
||||
filterOptions={(options, { inputValue }) => {
|
||||
const searchWords = inputValue
|
||||
.toLowerCase()
|
||||
.split(" ")
|
||||
.filter(Boolean);
|
||||
return options.filter((option) => {
|
||||
const optionWords = String(option.name)
|
||||
.toLowerCase()
|
||||
.split(" ");
|
||||
return searchWords.every((searchWord) =>
|
||||
optionWords.some((word) => word.startsWith(searchWord))
|
||||
);
|
||||
});
|
||||
}}
|
||||
renderOption={(props, option) => (
|
||||
<li {...props} key={option.id}>
|
||||
{String(option.name)}
|
||||
</li>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={linkItem}
|
||||
disabled={!selectedItemId}
|
||||
sx={{ alignSelf: "flex-start" }}
|
||||
>
|
||||
Добавить
|
||||
</Button>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
{isLoading && (
|
||||
<Typography color="textSecondary" textAlign="center" py={2}>
|
||||
Загрузка...
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<Typography color="error" textAlign="center" py={2}>
|
||||
{error}
|
||||
</Typography>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const LinkedStationsContents = observer(
|
||||
LinkedStationsContentsInner
|
||||
) as typeof LinkedStationsContentsInner;
|
||||
@@ -1 +1,2 @@
|
||||
export * from "./SightListPage";
|
||||
export { LinkedStations } from "./LinkedStations";
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
export const CarrierSvg = () => {
|
||||
return (
|
||||
<svg
|
||||
fill="#000000"
|
||||
height="26px"
|
||||
width="26px"
|
||||
version="1.1"
|
||||
id="Capa_1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlnsXlink="http://www.w3.org/1999/xlink"
|
||||
viewBox="0 0 489.785 489.785"
|
||||
>
|
||||
<g id="XMLID_196_">
|
||||
<path
|
||||
id="XMLID_203_"
|
||||
d="M409.772,379.327l-81.359-124.975c-5.884-9.054-15.925-13.119-25.987-13.119
|
||||
c-2.082,0-6.392,0.05-11.051,0.115c-0.363-0.61-0.742-1.215-1.355-1.627l-20.492-13.609c-2.364-1.569-5.434-1.486-7.701,0.182
|
||||
l-16.948,12.508l-16.959-12.508c-2.285-1.668-5.337-1.751-7.72-0.182l-20.455,13.609c-0.578,0.377-0.945,0.907-1.282,1.461
|
||||
c-4.828,0.031-9.327,0.057-11.222,0.057c-10.016,0-20.011,4.119-25.859,13.113L80.022,379.327
|
||||
c-8.65,13.267-5.149,31.008,7.896,39.992l18.06,12.449c10.887-25.926,28.868-48.094,51.45-64.279l4.657-7.162v3.861
|
||||
c16.364-10.811,34.941-18.477,54.885-22.234c-5.926-13.152-10.899-28.819-14.546-43.586c-4.249-17.232-6.741-33.201-6.741-42.245
|
||||
c0-3.351,0.433-6.579,1.09-9.727l14.8,48.975c0.766,2.565,2.984,4.417,5.641,4.73c0.268,0.03,0.529,0.046,0.784,0.046
|
||||
c2.365,0,4.602-1.25,5.818-3.34l11.538-19.873l3.246,3.235c-7.768,10.276-10.82,39.199-12.005,60.314
|
||||
c5.994-0.734,12.066-1.222,18.254-1.222c6.201,0,12.292,0.497,18.304,1.23c-1.186-21.114-4.237-50.037-12.024-60.322l3.246-3.255
|
||||
l11.574,19.892c1.216,2.09,3.422,3.34,5.805,3.34c0.255,0,0.522-0.016,0.779-0.046c2.655-0.314,4.874-2.166,5.659-4.73
|
||||
l14.791-48.872c0.634,3.116,1.051,6.313,1.051,9.624c0,16.806-8.425,57.342-21.276,85.831
|
||||
c19.981,3.768,38.588,11.453,54.953,22.291v-3.899l4.735,7.256c22.504,16.193,40.436,38.324,51.293,64.206l18.139-12.488
|
||||
C414.919,410.335,418.403,392.594,409.772,379.327z M219.962,276.685l-8.613-28.53l12.388-8.24l12.322,9.088L219.962,276.685z
|
||||
M269.783,276.685l-16.079-27.683l12.31-9.088l12.401,8.24L269.783,276.685z"
|
||||
/>
|
||||
<path
|
||||
id="XMLID_202_"
|
||||
d="M202.716,424.721l14.705,19.349c8.151-4.914,17.598-7.607,27.427-7.607c9.848,0,19.313,2.692,27.464,7.615
|
||||
l14.705-19.363c-11.465-10.799-26.346-16.721-42.15-16.721C229.055,407.994,214.156,413.925,202.716,424.721z"
|
||||
/>
|
||||
<path
|
||||
id="XMLID_201_"
|
||||
d="M176.693,160.576c0.499,25.456,14.96,47.266,36.03,58.591c9.622,5.18,20.473,8.384,32.174,8.384
|
||||
c11.683,0,22.503-3.198,32.114-8.368c21.063-11.311,35.579-33.117,36.06-58.582c-17.379,12.075-41.896,19.923-68.174,19.923
|
||||
S194.096,172.676,176.693,160.576z"
|
||||
/>
|
||||
<path
|
||||
id="XMLID_200_"
|
||||
d="M174.741,100.132l-0.225,20.205c0.037,15.991,31.524,36.82,70.38,36.82
|
||||
c38.855,0,70.314-20.829,70.331-36.82l-0.207-20.195c10.224-2.662,18.158-6.617,23.239-12.301
|
||||
c3.981-4.434,6.267-9.902,6.267-16.783C344.528,39.883,299.879,0,244.897,0c-55.031,0-99.631,39.883-99.631,71.058
|
||||
c0,6.881,2.273,12.34,6.236,16.783C156.585,93.524,164.529,97.479,174.741,100.132z"
|
||||
/>
|
||||
<path
|
||||
id="XMLID_197_"
|
||||
d="M244.848,356.925c-73.255,0-132.858,59.605-132.858,132.86h33.47c0-0.048,0-0.114,0-0.161v-0.031
|
||||
c1.088-6.557,6.711-11.334,13.313-11.334c0.115,0,0.243,0.01,0.37,0.01l51.707,1.341c-0.973,3.247-1.648,6.619-1.648,10.176h71.322
|
||||
c0-3.557-0.669-6.929-1.66-10.176l51.724-1.341c0.109,0,0.219-0.01,0.353-0.01c6.595,0,12.243,4.777,13.324,11.334v0.031
|
||||
c0,0.047,0,0.113,0,0.161h33.44C377.706,416.53,318.122,356.925,244.848,356.925z M302.201,433.91l-27.562,36.317
|
||||
c-6.389-9.687-17.325-16.104-29.792-16.104c-12.437,0-23.385,6.411-29.762,16.098l-27.555-36.3
|
||||
c-4.699-6.194-4.11-14.923,1.392-20.424c15.452-15.443,35.689-23.166,55.943-23.166c20.249,0,40.484,7.723,55.961,23.179
|
||||
C306.322,419.007,306.901,427.719,302.201,433.91z"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
58
src/shared/config/carrier.svg
Normal file
@@ -0,0 +1,58 @@
|
||||
<svg
|
||||
fill="#000000"
|
||||
height="26px"
|
||||
width="26px"
|
||||
version="1.1"
|
||||
id="Capa_1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlnsXlink="http://www.w3.org/1999/xlink"
|
||||
viewBox="0 0 489.785 489.785"
|
||||
>
|
||||
<g id="XMLID_196_">
|
||||
<path
|
||||
id="XMLID_203_"
|
||||
d="M409.772,379.327l-81.359-124.975c-5.884-9.054-15.925-13.119-25.987-13.119
|
||||
c-2.082,0-6.392,0.05-11.051,0.115c-0.363-0.61-0.742-1.215-1.355-1.627l-20.492-13.609c-2.364-1.569-5.434-1.486-7.701,0.182
|
||||
l-16.948,12.508l-16.959-12.508c-2.285-1.668-5.337-1.751-7.72-0.182l-20.455,13.609c-0.578,0.377-0.945,0.907-1.282,1.461
|
||||
c-4.828,0.031-9.327,0.057-11.222,0.057c-10.016,0-20.011,4.119-25.859,13.113L80.022,379.327
|
||||
c-8.65,13.267-5.149,31.008,7.896,39.992l18.06,12.449c10.887-25.926,28.868-48.094,51.45-64.279l4.657-7.162v3.861
|
||||
c16.364-10.811,34.941-18.477,54.885-22.234c-5.926-13.152-10.899-28.819-14.546-43.586c-4.249-17.232-6.741-33.201-6.741-42.245
|
||||
c0-3.351,0.433-6.579,1.09-9.727l14.8,48.975c0.766,2.565,2.984,4.417,5.641,4.73c0.268,0.03,0.529,0.046,0.784,0.046
|
||||
c2.365,0,4.602-1.25,5.818-3.34l11.538-19.873l3.246,3.235c-7.768,10.276-10.82,39.199-12.005,60.314
|
||||
c5.994-0.734,12.066-1.222,18.254-1.222c6.201,0,12.292,0.497,18.304,1.23c-1.186-21.114-4.237-50.037-12.024-60.322l3.246-3.255
|
||||
l11.574,19.892c1.216,2.09,3.422,3.34,5.805,3.34c0.255,0,0.522-0.016,0.779-0.046c2.655-0.314,4.874-2.166,5.659-4.73
|
||||
l14.791-48.872c0.634,3.116,1.051,6.313,1.051,9.624c0,16.806-8.425,57.342-21.276,85.831
|
||||
c19.981,3.768,38.588,11.453,54.953,22.291v-3.899l4.735,7.256c22.504,16.193,40.436,38.324,51.293,64.206l18.139-12.488
|
||||
C414.919,410.335,418.403,392.594,409.772,379.327z M219.962,276.685l-8.613-28.53l12.388-8.24l12.322,9.088L219.962,276.685z
|
||||
M269.783,276.685l-16.079-27.683l12.31-9.088l12.401,8.24L269.783,276.685z"
|
||||
/>
|
||||
<path
|
||||
id="XMLID_202_"
|
||||
d="M202.716,424.721l14.705,19.349c8.151-4.914,17.598-7.607,27.427-7.607c9.848,0,19.313,2.692,27.464,7.615
|
||||
l14.705-19.363c-11.465-10.799-26.346-16.721-42.15-16.721C229.055,407.994,214.156,413.925,202.716,424.721z"
|
||||
/>
|
||||
<path
|
||||
id="XMLID_201_"
|
||||
d="M176.693,160.576c0.499,25.456,14.96,47.266,36.03,58.591c9.622,5.18,20.473,8.384,32.174,8.384
|
||||
c11.683,0,22.503-3.198,32.114-8.368c21.063-11.311,35.579-33.117,36.06-58.582c-17.379,12.075-41.896,19.923-68.174,19.923
|
||||
S194.096,172.676,176.693,160.576z"
|
||||
/>
|
||||
<path
|
||||
id="XMLID_200_"
|
||||
d="M174.741,100.132l-0.225,20.205c0.037,15.991,31.524,36.82,70.38,36.82
|
||||
c38.855,0,70.314-20.829,70.331-36.82l-0.207-20.195c10.224-2.662,18.158-6.617,23.239-12.301
|
||||
c3.981-4.434,6.267-9.902,6.267-16.783C344.528,39.883,299.879,0,244.897,0c-55.031,0-99.631,39.883-99.631,71.058
|
||||
c0,6.881,2.273,12.34,6.236,16.783C156.585,93.524,164.529,97.479,174.741,100.132z"
|
||||
/>
|
||||
<path
|
||||
id="XMLID_197_"
|
||||
d="M244.848,356.925c-73.255,0-132.858,59.605-132.858,132.86h33.47c0-0.048,0-0.114,0-0.161v-0.031
|
||||
c1.088-6.557,6.711-11.334,13.313-11.334c0.115,0,0.243,0.01,0.37,0.01l51.707,1.341c-0.973,3.247-1.648,6.619-1.648,10.176h71.322
|
||||
c0-3.557-0.669-6.929-1.66-10.176l51.724-1.341c0.109,0,0.219-0.01,0.353-0.01c6.595,0,12.243,4.777,13.324,11.334v0.031
|
||||
c0,0.047,0,0.113,0,0.161h33.44C377.706,416.53,318.122,356.925,244.848,356.925z M302.201,433.91l-27.562,36.317
|
||||
c-6.389-9.687-17.325-16.104-29.792-16.104c-12.437,0-23.385,6.411-29.762,16.098l-27.555-36.3
|
||||
c-4.699-6.194-4.11-14.923,1.392-20.424c15.452-15.443,35.689-23.166,55.943-23.166c20.249,0,40.484,7.723,55.961,23.179
|
||||
C306.322,419.007,306.901,427.719,302.201,433.91z"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.4 KiB |
@@ -16,7 +16,8 @@ import {
|
||||
Cpu,
|
||||
// BookImage,
|
||||
} from "lucide-react";
|
||||
import { CarrierSvg } from "./CarrierSvg";
|
||||
|
||||
import carrierIcon from "./carrier.svg";
|
||||
|
||||
export const DRAWER_WIDTH = 300;
|
||||
|
||||
@@ -123,7 +124,7 @@ export const NAVIGATION_ITEMS: {
|
||||
id: "carriers",
|
||||
label: "Перевозчики",
|
||||
// @ts-ignore
|
||||
icon: CarrierSvg,
|
||||
icon: () => <img src={carrierIcon} alt="Перевозчики"/>,
|
||||
path: "/carrier",
|
||||
for_admin: true,
|
||||
},
|
||||
|
||||
@@ -1,4 +1,22 @@
|
||||
import * as countries from "i18n-iso-countries";
|
||||
import * as ru from "i18n-iso-countries/langs/ru.json";
|
||||
import * as en from "i18n-iso-countries/langs/en.json";
|
||||
import * as zh from "i18n-iso-countries/langs/zh.json";
|
||||
|
||||
countries.registerLocale(ru);
|
||||
countries.registerLocale(en);
|
||||
countries.registerLocale(zh);
|
||||
|
||||
const generateCountriesList = (locale: string) => {
|
||||
const names = countries.getNames(locale);
|
||||
return Object.entries(names).map(([code, name]) => ({
|
||||
code: code,
|
||||
name: name,
|
||||
}));
|
||||
};
|
||||
|
||||
export const API_URL = import.meta.env.VITE_API_URL;
|
||||
|
||||
export const MEDIA_TYPE_LABELS = {
|
||||
1: "Фото",
|
||||
2: "Видео",
|
||||
@@ -22,751 +40,6 @@ export const MEDIA_TYPE_VALUES = {
|
||||
video_preview: 2,
|
||||
};
|
||||
|
||||
export const RU_COUNTRIES = [
|
||||
{ code: "AF", name: "Афганистан" },
|
||||
{ code: "AX", name: "Аландские острова" },
|
||||
{ code: "AL", name: "Албания" },
|
||||
{ code: "DZ", name: "Алжир" },
|
||||
{ code: "AS", name: "Американское Самоа" },
|
||||
{ code: "AD", name: "Андорра" },
|
||||
{ code: "AO", name: "Ангола" },
|
||||
{ code: "AI", name: "Ангилья" },
|
||||
{ code: "AQ", name: "Антарктида" },
|
||||
{ code: "AG", name: "Антигуа и Барбуда" },
|
||||
{ code: "AR", name: "Аргентина" },
|
||||
{ code: "AM", name: "Армения" },
|
||||
{ code: "AW", name: "Аруба" },
|
||||
{ code: "AU", name: "Австралия" },
|
||||
{ code: "AT", name: "Австрия" },
|
||||
{ code: "AZ", name: "Азербайджан" },
|
||||
{ code: "BS", name: "Багамы" },
|
||||
{ code: "BH", name: "Бахрейн" },
|
||||
{ code: "BD", name: "Бангладеш" },
|
||||
{ code: "BB", name: "Барбадос" },
|
||||
{ code: "BY", name: "Беларусь" },
|
||||
{ code: "BE", name: "Бельгия" },
|
||||
{ code: "BZ", name: "Белиз" },
|
||||
{ code: "BJ", name: "Бенин" },
|
||||
{ code: "BM", name: "Бермуды" },
|
||||
{ code: "BT", name: "Бутан" },
|
||||
{ code: "BO", name: "Боливия" },
|
||||
{ code: "BA", name: "Босния и Герцеговина" },
|
||||
{ code: "BW", name: "Ботсвана" },
|
||||
{ code: "BV", name: "Остров Буве" },
|
||||
{ code: "BR", name: "Бразилия" },
|
||||
{ code: "IO", name: "Британская территория в Индийском океане" },
|
||||
{ code: "BN", name: "Бруней-Даруссалам" },
|
||||
{ code: "BG", name: "Болгария" },
|
||||
{ code: "BF", name: "Буркина-Фасо" },
|
||||
{ code: "BI", name: "Бурунди" },
|
||||
{ code: "KH", name: "Камбоджа" },
|
||||
{ code: "CM", name: "Камерун" },
|
||||
{ code: "CA", name: "Канада" },
|
||||
{ code: "CV", name: "Кабо-Верде" },
|
||||
{ code: "KY", name: "Каймановы острова" },
|
||||
{ code: "CF", name: "Центральноафриканская Республика" },
|
||||
{ code: "TD", name: "Чад" },
|
||||
{ code: "CL", name: "Чили" },
|
||||
{ code: "CN", name: "Китай" },
|
||||
{ code: "CX", name: "Остров Рождества" },
|
||||
{ code: "CC", name: "Кокосовые (Килинг) острова" },
|
||||
{ code: "CO", name: "Колумбия" },
|
||||
{ code: "KM", name: "Коморы" },
|
||||
{ code: "CG", name: "Конго" },
|
||||
{ code: "CD", name: "Демократическая Республика Конго" },
|
||||
{ code: "CK", name: "Острова Кука" },
|
||||
{ code: "CR", name: "Коста-Рика" },
|
||||
{ code: "CI", name: "Кот-д'Ивуар" },
|
||||
{ code: "HR", name: "Хорватия" },
|
||||
{ code: "CU", name: "Куба" },
|
||||
{ code: "CY", name: "Кипр" },
|
||||
{ code: "CZ", name: "Чехия" },
|
||||
{ code: "DK", name: "Дания" },
|
||||
{ code: "DJ", name: "Джибути" },
|
||||
{ code: "DM", name: "Доминика" },
|
||||
{ code: "DO", name: "Доминиканская Республика" },
|
||||
{ code: "EC", name: "Эквадор" },
|
||||
{ code: "EG", name: "Египет" },
|
||||
{ code: "SV", name: "Сальвадор" },
|
||||
{ code: "GQ", name: "Экваториальная Гвинея" },
|
||||
{ code: "ER", name: "Эритрея" },
|
||||
{ code: "EE", name: "Эстония" },
|
||||
{ code: "ET", name: "Эфиопия" },
|
||||
{ code: "FK", name: "Фолклендские острова (Мальвинские)" },
|
||||
{ code: "FO", name: "Фарерские острова" },
|
||||
{ code: "FJ", name: "Фиджи" },
|
||||
{ code: "FI", name: "Финляндия" },
|
||||
{ code: "FR", name: "Франция" },
|
||||
{ code: "GF", name: "Французская Гвиана" },
|
||||
{ code: "PF", name: "Французская Полинезия" },
|
||||
{ code: "TF", name: "Французские Южные территории" },
|
||||
{ code: "GA", name: "Габон" },
|
||||
{ code: "GM", name: "Гамбия" },
|
||||
{ code: "GE", name: "Грузия" },
|
||||
{ code: "DE", name: "Германия" },
|
||||
{ code: "GH", name: "Гана" },
|
||||
{ code: "GI", name: "Гибралтар" },
|
||||
{ code: "GR", name: "Греция" },
|
||||
{ code: "GL", name: "Гренландия" },
|
||||
{ code: "GD", name: "Гренада" },
|
||||
{ code: "GP", name: "Гваделупа" },
|
||||
{ code: "GU", name: "Гуам" },
|
||||
{ code: "GT", name: "Гватемала" },
|
||||
{ code: "GG", name: "Гернси" },
|
||||
{ code: "GN", name: "Гвинея" },
|
||||
{ code: "GW", name: "Гвинея-Бисау" },
|
||||
{ code: "GY", name: "Гайана" },
|
||||
{ code: "HT", name: "Гаити" },
|
||||
{ code: "HM", name: "Остров Херд и острова Макдональд" },
|
||||
{ code: "VA", name: "Ватикан" },
|
||||
{ code: "HN", name: "Гондурас" },
|
||||
{ code: "HK", name: "Гонконг" },
|
||||
{ code: "HU", name: "Венгрия" },
|
||||
{ code: "IS", name: "Исландия" },
|
||||
{ code: "IN", name: "Индия" },
|
||||
{ code: "ID", name: "Индонезия" },
|
||||
{ code: "IR", name: "Иран" },
|
||||
{ code: "IQ", name: "Ирак" },
|
||||
{ code: "IE", name: "Ирландия" },
|
||||
{ code: "IM", name: "Остров Мэн" },
|
||||
{ code: "IL", name: "Израиль" },
|
||||
{ code: "IT", name: "Италия" },
|
||||
{ code: "JM", name: "Ямайка" },
|
||||
{ code: "JP", name: "Япония" },
|
||||
{ code: "JE", name: "Джерси" },
|
||||
{ code: "JO", name: "Иордания" },
|
||||
{ code: "KZ", name: "Казахстан" },
|
||||
{ code: "KE", name: "Кения" },
|
||||
{ code: "KI", name: "Кирибати" },
|
||||
{ code: "KR", name: "Корея" },
|
||||
{ code: "KP", name: "Северная Корея" },
|
||||
{ code: "KW", name: "Кувейт" },
|
||||
{ code: "KG", name: "Киргизия" },
|
||||
{ code: "LA", name: "Лаос" },
|
||||
{ code: "LV", name: "Латвия" },
|
||||
{ code: "LB", name: "Ливан" },
|
||||
{ code: "LS", name: "Лесото" },
|
||||
{ code: "LR", name: "Либерия" },
|
||||
{ code: "LY", name: "Ливия" },
|
||||
{ code: "LI", name: "Лихтенштейн" },
|
||||
{ code: "LT", name: "Литва" },
|
||||
{ code: "LU", name: "Люксембург" },
|
||||
{ code: "MO", name: "Макао" },
|
||||
{ code: "MK", name: "Северная Македония" },
|
||||
{ code: "MG", name: "Мадагаскар" },
|
||||
{ code: "MW", name: "Малави" },
|
||||
{ code: "MY", name: "Малайзия" },
|
||||
{ code: "MV", name: "Мальдивы" },
|
||||
{ code: "ML", name: "Мали" },
|
||||
{ code: "MT", name: "Мальта" },
|
||||
{ code: "MH", name: "Маршалловы Острова" },
|
||||
{ code: "MQ", name: "Мартиника" },
|
||||
{ code: "MR", name: "Мавритания" },
|
||||
{ code: "MU", name: "Маврикий" },
|
||||
{ code: "YT", name: "Майотта" },
|
||||
{ code: "MX", name: "Мексика" },
|
||||
{ code: "FM", name: "Микронезия" },
|
||||
{ code: "MD", name: "Молдова" },
|
||||
{ code: "MC", name: "Монако" },
|
||||
{ code: "MN", name: "Монголия" },
|
||||
{ code: "ME", name: "Черногория" },
|
||||
{ code: "MS", name: "Монтсеррат" },
|
||||
{ code: "MA", name: "Марокко" },
|
||||
{ code: "MZ", name: "Мозамбик" },
|
||||
{ code: "MM", name: "Мьянма" },
|
||||
{ code: "NA", name: "Намибия" },
|
||||
{ code: "NR", name: "Науру" },
|
||||
{ code: "NP", name: "Непал" },
|
||||
{ code: "NL", name: "Нидерланды" },
|
||||
{ code: "AN", name: "Нидерландские Антильские острова" },
|
||||
{ code: "NC", name: "Новая Каледония" },
|
||||
{ code: "NZ", name: "Новая Зеландия" },
|
||||
{ code: "NI", name: "Никарагуа" },
|
||||
{ code: "NE", name: "Нигер" },
|
||||
{ code: "NG", name: "Нигерия" },
|
||||
{ code: "NU", name: "Ниуэ" },
|
||||
{ code: "NF", name: "Остров Норфолк" },
|
||||
{ code: "MP", name: "Северные Марианские острова" },
|
||||
{ code: "NO", name: "Норвегия" },
|
||||
{ code: "OM", name: "Оман" },
|
||||
{ code: "PK", name: "Пакистан" },
|
||||
{ code: "PW", name: "Палау" },
|
||||
{ code: "PS", name: "Палестинская территория" },
|
||||
{ code: "PA", name: "Панама" },
|
||||
{ code: "PG", name: "Папуа — Новая Гвинея" },
|
||||
{ code: "PY", name: "Парагвай" },
|
||||
{ code: "PE", name: "Перу" },
|
||||
{ code: "PH", name: "Филиппины" },
|
||||
{ code: "PN", name: "Питкэрн" },
|
||||
{ code: "PL", name: "Польша" },
|
||||
{ code: "PT", name: "Португалия" },
|
||||
{ code: "PR", name: "Пуэрто-Рико" },
|
||||
{ code: "QA", name: "Катар" },
|
||||
{ code: "RE", name: "Реюньон" },
|
||||
{ code: "RO", name: "Румыния" },
|
||||
{ code: "RU", name: "Россия" },
|
||||
{ code: "RW", name: "Руанда" },
|
||||
{ code: "BL", name: "Сен-Бартелеми" },
|
||||
{ code: "SH", name: "Остров Святой Елены" },
|
||||
{ code: "KN", name: "Сент-Китс и Невис" },
|
||||
{ code: "LC", name: "Сент-Люсия" },
|
||||
{ code: "MF", name: "Сен-Мартен" },
|
||||
{ code: "PM", name: "Сен-Пьер и Микелон" },
|
||||
{ code: "VC", name: "Сент-Винсент и Гренадины" },
|
||||
{ code: "WS", name: "Самоа" },
|
||||
{ code: "SM", name: "Сан-Марино" },
|
||||
{ code: "ST", name: "Сан-Томе и Принсипи" },
|
||||
{ code: "SA", name: "Саудовская Аравия" },
|
||||
{ code: "SN", name: "Сенегал" },
|
||||
{ code: "RS", name: "Сербия" },
|
||||
{ code: "SC", name: "Сейшельские Острова" },
|
||||
{ code: "SL", name: "Сьерра-Леоне" },
|
||||
{ code: "SG", name: "Сингапур" },
|
||||
{ code: "SK", name: "Словакия" },
|
||||
{ code: "SI", name: "Словения" },
|
||||
{ code: "SB", name: "Соломоновы Острова" },
|
||||
{ code: "SO", name: "Сомали" },
|
||||
{ code: "ZA", name: "Южная Африка" },
|
||||
{ code: "GS", name: "Южная Георгия и Южные Сандвичевы острова" },
|
||||
{ code: "ES", name: "Испания" },
|
||||
{ code: "LK", name: "Шри-Ланка" },
|
||||
{ code: "SD", name: "Судан" },
|
||||
{ code: "SR", name: "Суринам" },
|
||||
{ code: "SJ", name: "Шпицберген и Ян-Майен" },
|
||||
{ code: "SZ", name: "Свазиленд" },
|
||||
{ code: "SE", name: "Швеция" },
|
||||
{ code: "CH", name: "Швейцария" },
|
||||
{ code: "SY", name: "Сирия" },
|
||||
{ code: "TW", name: "Тайвань" },
|
||||
{ code: "TJ", name: "Таджикистан" },
|
||||
{ code: "TZ", name: "Танзания" },
|
||||
{ code: "TH", name: "Таиланд" },
|
||||
{ code: "TL", name: "Восточный Тимор" },
|
||||
{ code: "TG", name: "Того" },
|
||||
{ code: "TK", name: "Токелау" },
|
||||
{ code: "TO", name: "Тонга" },
|
||||
{ code: "TT", name: "Тринидад и Тобаго" },
|
||||
{ code: "TN", name: "Тунис" },
|
||||
{ code: "TR", name: "Турция" },
|
||||
{ code: "TM", name: "Туркмения" },
|
||||
{ code: "TC", name: "Теркс и Кайкос" },
|
||||
{ code: "TV", name: "Тувалу" },
|
||||
{ code: "UG", name: "Уганда" },
|
||||
{ code: "UA", name: "Украина" },
|
||||
{ code: "AE", name: "Объединённые Арабские Эмираты" },
|
||||
{ code: "GB", name: "Великобритания" },
|
||||
{ code: "US", name: "США" },
|
||||
{ code: "UM", name: "Внешние малые острова США" },
|
||||
{ code: "UY", name: "Уругвай" },
|
||||
{ code: "UZ", name: "Узбекистан" },
|
||||
{ code: "VU", name: "Вануату" },
|
||||
{ code: "VE", name: "Венесуэла" },
|
||||
{ code: "VN", name: "Вьетнам" },
|
||||
{ code: "VG", name: "Британские Виргинские острова" },
|
||||
{ code: "VI", name: "Виргинские острова (США)" },
|
||||
{ code: "WF", name: "Уоллис и Футуна" },
|
||||
{ code: "EH", name: "Западная Сахара" },
|
||||
{ code: "YE", name: "Йемен" },
|
||||
{ code: "ZM", name: "Замбия" },
|
||||
{ code: "ZW", name: "Зимбабве" },
|
||||
];
|
||||
|
||||
// countries-en.js
|
||||
export const EN_COUNTRIES = [
|
||||
{ code: "AF", name: "Afghanistan" },
|
||||
{ code: "AX", name: "Aland Islands" },
|
||||
{ code: "AL", name: "Albania" },
|
||||
{ code: "DZ", name: "Algeria" },
|
||||
{ code: "AS", name: "American Samoa" },
|
||||
{ code: "AD", name: "Andorra" },
|
||||
{ code: "AO", name: "Angola" },
|
||||
{ code: "AI", name: "Anguilla" },
|
||||
{ code: "AQ", name: "Antarctica" },
|
||||
{ code: "AG", name: "Antigua And Barbuda" },
|
||||
{ code: "AR", name: "Argentina" },
|
||||
{ code: "AM", name: "Armenia" },
|
||||
{ code: "AW", name: "Aruba" },
|
||||
{ code: "AU", name: "Australia" },
|
||||
{ code: "AT", name: "Austria" },
|
||||
{ code: "AZ", name: "Azerbaijan" },
|
||||
{ code: "BS", name: "Bahamas" },
|
||||
{ code: "BH", name: "Bahrain" },
|
||||
{ code: "BD", name: "Bangladesh" },
|
||||
{ code: "BB", name: "Barbados" },
|
||||
{ code: "BY", name: "Belarus" },
|
||||
{ code: "BE", name: "Belgium" },
|
||||
{ code: "BZ", name: "Belize" },
|
||||
{ code: "BJ", name: "Benin" },
|
||||
{ code: "BM", name: "Bermuda" },
|
||||
{ code: "BT", name: "Bhutan" },
|
||||
{ code: "BO", name: "Bolivia" },
|
||||
{ code: "BA", name: "Bosnia And Herzegovina" },
|
||||
{ code: "BW", name: "Botswana" },
|
||||
{ code: "BV", name: "Bouvet Island" },
|
||||
{ code: "BR", name: "Brazil" },
|
||||
{ code: "IO", name: "British Indian Ocean Territory" },
|
||||
{ code: "BN", name: "Brunei Darussalam" },
|
||||
{ code: "BG", name: "Bulgaria" },
|
||||
{ code: "BF", name: "Burkina Faso" },
|
||||
{ code: "BI", name: "Burundi" },
|
||||
{ code: "KH", name: "Cambodia" },
|
||||
{ code: "CM", name: "Cameroon" },
|
||||
{ code: "CA", name: "Canada" },
|
||||
{ code: "CV", name: "Cape Verde" },
|
||||
{ code: "KY", name: "Cayman Islands" },
|
||||
{ code: "CF", name: "Central African Republic" },
|
||||
{ code: "TD", name: "Chad" },
|
||||
{ code: "CL", name: "Chile" },
|
||||
{ code: "CN", name: "China" },
|
||||
{ code: "CX", name: "Christmas Island" },
|
||||
{ code: "CC", name: "Cocos (Keeling) Islands" },
|
||||
{ code: "CO", name: "Colombia" },
|
||||
{ code: "KM", name: "Comoros" },
|
||||
{ code: "CG", name: "Congo" },
|
||||
{ code: "CD", name: "Congo, Democratic Republic" },
|
||||
{ code: "CK", name: "Cook Islands" },
|
||||
{ code: "CR", name: "Costa Rica" },
|
||||
{ code: "CI", name: "Cote D'Ivoire" },
|
||||
{ code: "HR", name: "Croatia" },
|
||||
{ code: "CU", name: "Cuba" },
|
||||
{ code: "CY", name: "Cyprus" },
|
||||
{ code: "CZ", name: "Czech Republic" },
|
||||
{ code: "DK", name: "Denmark" },
|
||||
{ code: "DJ", name: "Djibouti" },
|
||||
{ code: "DM", name: "Dominica" },
|
||||
{ code: "DO", name: "Dominican Republic" },
|
||||
{ code: "EC", name: "Ecuador" },
|
||||
{ code: "EG", name: "Egypt" },
|
||||
{ code: "SV", name: "El Salvador" },
|
||||
{ code: "GQ", name: "Equatorial Guinea" },
|
||||
{ code: "ER", name: "Eritrea" },
|
||||
{ code: "EE", name: "Estonia" },
|
||||
{ code: "ET", name: "Ethiopia" },
|
||||
{ code: "FK", name: "Falkland Islands (Malvinas)" },
|
||||
{ code: "FO", name: "Faroe Islands" },
|
||||
{ code: "FJ", name: "Fiji" },
|
||||
{ code: "FI", name: "Finland" },
|
||||
{ code: "FR", name: "France" },
|
||||
{ code: "GF", name: "French Guiana" },
|
||||
{ code: "PF", name: "French Polynesia" },
|
||||
{ code: "TF", name: "French Southern Territories" },
|
||||
{ code: "GA", name: "Gabon" },
|
||||
{ code: "GM", name: "Gambia" },
|
||||
{ code: "GE", name: "Georgia" },
|
||||
{ code: "DE", name: "Germany" },
|
||||
{ code: "GH", name: "Ghana" },
|
||||
{ code: "GI", name: "Gibraltar" },
|
||||
{ code: "GR", name: "Greece" },
|
||||
{ code: "GL", name: "Greenland" },
|
||||
{ code: "GD", name: "Grenada" },
|
||||
{ code: "GP", name: "Guadeloupe" },
|
||||
{ code: "GU", name: "Guam" },
|
||||
{ code: "GT", name: "Guatemala" },
|
||||
{ code: "GG", name: "Guernsey" },
|
||||
{ code: "GN", name: "Guinea" },
|
||||
{ code: "GW", name: "Guinea-Bissau" },
|
||||
{ code: "GY", name: "Guyana" },
|
||||
{ code: "HT", name: "Haiti" },
|
||||
{ code: "HM", name: "Heard Island & Mcdonald Islands" },
|
||||
{ code: "VA", name: "Holy See (Vatican City State)" },
|
||||
{ code: "HN", name: "Honduras" },
|
||||
{ code: "HK", name: "Hong Kong" },
|
||||
{ code: "HU", name: "Hungary" },
|
||||
{ code: "IS", name: "Iceland" },
|
||||
{ code: "IN", name: "India" },
|
||||
{ code: "ID", name: "Indonesia" },
|
||||
{ code: "IR", name: "Iran, Islamic Republic Of" },
|
||||
{ code: "IQ", name: "Iraq" },
|
||||
{ code: "IE", name: "Ireland" },
|
||||
{ code: "IM", name: "Isle Of Man" },
|
||||
{ code: "IL", name: "Israel" },
|
||||
{ code: "IT", name: "Italy" },
|
||||
{ code: "JM", name: "Jamaica" },
|
||||
{ code: "JP", name: "Japan" },
|
||||
{ code: "JE", name: "Jersey" },
|
||||
{ code: "JO", name: "Jordan" },
|
||||
{ code: "KZ", name: "Kazakhstan" },
|
||||
{ code: "KE", name: "Kenya" },
|
||||
{ code: "KI", name: "Kiribati" },
|
||||
{ code: "KR", name: "Korea" },
|
||||
{ code: "KP", name: "North Korea" },
|
||||
{ code: "KW", name: "Kuwait" },
|
||||
{ code: "KG", name: "Kyrgyzstan" },
|
||||
{ code: "LA", name: "Lao People's Democratic Republic" },
|
||||
{ code: "LV", name: "Latvia" },
|
||||
{ code: "LB", name: "Lebanon" },
|
||||
{ code: "LS", name: "Lesotho" },
|
||||
{ code: "LR", name: "Liberia" },
|
||||
{ code: "LY", name: "Libyan Arab Jamahiriya" },
|
||||
{ code: "LI", name: "Liechtenstein" },
|
||||
{ code: "LT", name: "Lithuania" },
|
||||
{ code: "LU", name: "Luxembourg" },
|
||||
{ code: "MO", name: "Macao" },
|
||||
{ code: "MK", name: "Macedonia" },
|
||||
{ code: "MG", name: "Madagascar" },
|
||||
{ code: "MW", name: "Malawi" },
|
||||
{ code: "MY", name: "Malaysia" },
|
||||
{ code: "MV", name: "Maldives" },
|
||||
{ code: "ML", name: "Mali" },
|
||||
{ code: "MT", name: "Malta" },
|
||||
{ code: "MH", name: "Marshall Islands" },
|
||||
{ code: "MQ", name: "Martinique" },
|
||||
{ code: "MR", name: "Mauritania" },
|
||||
{ code: "MU", name: "Mauritius" },
|
||||
{ code: "YT", name: "Mayotte" },
|
||||
{ code: "MX", name: "Mexico" },
|
||||
{ code: "FM", name: "Micronesia, Federated States Of" },
|
||||
{ code: "MD", name: "Moldova" },
|
||||
{ code: "MC", name: "Monaco" },
|
||||
{ code: "MN", name: "Mongolia" },
|
||||
{ code: "ME", name: "Montenegro" },
|
||||
{ code: "MS", name: "Montserrat" },
|
||||
{ code: "MA", name: "Morocco" },
|
||||
{ code: "MZ", name: "Mozambique" },
|
||||
{ code: "MM", name: "Myanmar" },
|
||||
{ code: "NA", name: "Namibia" },
|
||||
{ code: "NR", name: "Nauru" },
|
||||
{ code: "NP", name: "Nepal" },
|
||||
{ code: "NL", name: "Netherlands" },
|
||||
{ code: "AN", name: "Netherlands Antilles" },
|
||||
{ code: "NC", name: "New Caledonia" },
|
||||
{ code: "NZ", name: "New Zealand" },
|
||||
{ code: "NI", name: "Nicaragua" },
|
||||
{ code: "NE", name: "Niger" },
|
||||
{ code: "NG", name: "Nigeria" },
|
||||
{ code: "NU", name: "Niue" },
|
||||
{ code: "NF", name: "Norfolk Island" },
|
||||
{ code: "MP", name: "Northern Mariana Islands" },
|
||||
{ code: "NO", name: "Norway" },
|
||||
{ code: "OM", name: "Oman" },
|
||||
{ code: "PK", name: "Pakistan" },
|
||||
{ code: "PW", name: "Palau" },
|
||||
{ code: "PS", name: "Palestinian Territory, Occupied" },
|
||||
{ code: "PA", name: "Panama" },
|
||||
{ code: "PG", name: "Papua New Guinea" },
|
||||
{ code: "PY", name: "Paraguay" },
|
||||
{ code: "PE", name: "Peru" },
|
||||
{ code: "PH", name: "Philippines" },
|
||||
{ code: "PN", name: "Pitcairn" },
|
||||
{ code: "PL", name: "Poland" },
|
||||
{ code: "PT", name: "Portugal" },
|
||||
{ code: "PR", name: "Puerto Rico" },
|
||||
{ code: "QA", name: "Qatar" },
|
||||
{ code: "RE", name: "Reunion" },
|
||||
{ code: "RO", name: "Romania" },
|
||||
{ code: "RU", name: "Russian Federation" },
|
||||
{ code: "RW", name: "Rwanda" },
|
||||
{ code: "BL", name: "Saint Barthelemy" },
|
||||
{ code: "SH", name: "Saint Helena" },
|
||||
{ code: "KN", name: "Saint Kitts And Nevis" },
|
||||
{ code: "LC", name: "Saint Lucia" },
|
||||
{ code: "MF", name: "Saint Martin" },
|
||||
{ code: "PM", name: "Saint Pierre And Miquelon" },
|
||||
{ code: "VC", name: "Saint Vincent And Grenadines" },
|
||||
{ code: "WS", name: "Samoa" },
|
||||
{ code: "SM", name: "San Marino" },
|
||||
{ code: "ST", name: "Sao Tome And Principe" },
|
||||
{ code: "SA", name: "Saudi Arabia" },
|
||||
{ code: "SN", name: "Senegal" },
|
||||
{ code: "RS", name: "Serbia" },
|
||||
{ code: "SC", name: "Seychelles" },
|
||||
{ code: "SL", name: "Sierra Leone" },
|
||||
{ code: "SG", name: "Singapore" },
|
||||
{ code: "SK", name: "Slovakia" },
|
||||
{ code: "SI", name: "Slovenia" },
|
||||
{ code: "SB", name: "Solomon Islands" },
|
||||
{ code: "SO", name: "Somalia" },
|
||||
{ code: "ZA", name: "South Africa" },
|
||||
{ code: "GS", name: "South Georgia And Sandwich Isl." },
|
||||
{ code: "ES", name: "Spain" },
|
||||
{ code: "LK", name: "Sri Lanka" },
|
||||
{ code: "SD", name: "Sudan" },
|
||||
{ code: "SR", name: "Suriname" },
|
||||
{ code: "SJ", name: "Svalbard And Jan Mayen" },
|
||||
{ code: "SZ", name: "Swaziland" },
|
||||
{ code: "SE", name: "Sweden" },
|
||||
{ code: "CH", name: "Switzerland" },
|
||||
{ code: "SY", name: "Syrian Arab Republic" },
|
||||
{ code: "TW", name: "Taiwan" },
|
||||
{ code: "TJ", name: "Tajikistan" },
|
||||
{ code: "TZ", name: "Tanzania" },
|
||||
{ code: "TH", name: "Thailand" },
|
||||
{ code: "TL", name: "Timor-Leste" },
|
||||
{ code: "TG", name: "Togo" },
|
||||
{ code: "TK", name: "Tokelau" },
|
||||
{ code: "TO", name: "Tonga" },
|
||||
{ code: "TT", name: "Trinidad And Tobago" },
|
||||
{ code: "TN", name: "Tunisia" },
|
||||
{ code: "TR", name: "Turkey" },
|
||||
{ code: "TM", name: "Turkmenistan" },
|
||||
{ code: "TC", name: "Turks And Caicos Islands" },
|
||||
{ code: "TV", name: "Tuvalu" },
|
||||
{ code: "UG", name: "Uganda" },
|
||||
{ code: "UA", name: "Ukraine" },
|
||||
{ code: "AE", name: "United Arab Emirates" },
|
||||
{ code: "GB", name: "United Kingdom" },
|
||||
{ code: "US", name: "United States" },
|
||||
{ code: "UM", name: "United States Outlying Islands" },
|
||||
{ code: "UY", name: "Uruguay" },
|
||||
{ code: "UZ", name: "Uzbekistan" },
|
||||
{ code: "VU", name: "Vanuatu" },
|
||||
{ code: "VE", name: "Venezuela" },
|
||||
{ code: "VN", name: "Vietnam" },
|
||||
{ code: "VG", name: "Virgin Islands, British" },
|
||||
{ code: "VI", name: "Virgin Islands, U.S." },
|
||||
{ code: "WF", name: "Wallis And Futuna" },
|
||||
{ code: "EH", name: "Western Sahara" },
|
||||
{ code: "YE", name: "Yemen" },
|
||||
{ code: "ZM", name: "Zambia" },
|
||||
{ code: "ZW", name: "Zimbabwe" },
|
||||
];
|
||||
|
||||
// countries-zh.js
|
||||
export const ZH_COUNTRIES = [
|
||||
{ code: "AF", name: "阿富汗" },
|
||||
{ code: "AX", name: "奥兰群岛" },
|
||||
{ code: "AL", name: "阿尔巴尼亚" },
|
||||
{ code: "DZ", name: "阿尔及利亚" },
|
||||
{ code: "AS", name: "美属萨摩亚" },
|
||||
{ code: "AD", name: "安道尔" },
|
||||
{ code: "AO", name: "安哥拉" },
|
||||
{ code: "AI", name: "安圭拉" },
|
||||
{ code: "AQ", name: "南极洲" },
|
||||
{ code: "AG", name: "安提瓜和巴布达" },
|
||||
{ code: "AR", name: "阿根廷" },
|
||||
{ code: "AM", name: "亚美尼亚" },
|
||||
{ code: "AW", name: "阿鲁巴" },
|
||||
{ code: "AU", name: "澳大利亚" },
|
||||
{ code: "AT", name: "奥地利" },
|
||||
{ code: "AZ", name: "阿塞拜疆" },
|
||||
{ code: "BS", name: "巴哈马" },
|
||||
{ code: "BH", name: "巴林" },
|
||||
{ code: "BD", name: "孟加拉国" },
|
||||
{ code: "BB", name: "巴巴多斯" },
|
||||
{ code: "BY", name: "白俄罗斯" },
|
||||
{ code: "BE", name: "比利时" },
|
||||
{ code: "BZ", name: "伯利兹" },
|
||||
{ code: "BJ", name: "贝宁" },
|
||||
{ code: "BM", name: "百慕大" },
|
||||
{ code: "BT", name: "不丹" },
|
||||
{ code: "BO", name: "玻利维亚" },
|
||||
{ code: "BA", name: "波斯尼亚和黑塞哥维那" },
|
||||
{ code: "BW", name: "博茨瓦纳" },
|
||||
{ code: "BV", name: "布韦岛" },
|
||||
{ code: "BR", name: "巴西" },
|
||||
{ code: "IO", name: "英属印度洋领地" },
|
||||
{ code: "BN", name: "文莱" },
|
||||
{ code: "BG", name: "保加利亚" },
|
||||
{ code: "BF", name: "布基纳法索" },
|
||||
{ code: "BI", name: "布隆迪" },
|
||||
{ code: "KH", name: "柬埔寨" },
|
||||
{ code: "CM", name: "喀麦隆" },
|
||||
{ code: "CA", name: "加拿大" },
|
||||
{ code: "CV", name: "佛得角" },
|
||||
{ code: "KY", name: "开曼群岛" },
|
||||
{ code: "CF", name: "中非共和国" },
|
||||
{ code: "TD", name: "乍得" },
|
||||
{ code: "CL", name: "智利" },
|
||||
{ code: "CN", name: "中国" },
|
||||
{ code: "CX", name: "圣诞岛" },
|
||||
{ code: "CC", name: "科科斯(基林)群岛" },
|
||||
{ code: "CO", name: "哥伦比亚" },
|
||||
{ code: "KM", name: "科摩罗" },
|
||||
{ code: "CG", name: "刚果" },
|
||||
{ code: "CD", name: "刚果(金)" },
|
||||
{ code: "CK", name: "库克群岛" },
|
||||
{ code: "CR", name: "哥斯达黎加" },
|
||||
{ code: "CI", name: "科特迪瓦" },
|
||||
{ code: "HR", name: "克罗地亚" },
|
||||
{ code: "CU", name: "古巴" },
|
||||
{ code: "CY", name: "塞浦路斯" },
|
||||
{ code: "CZ", name: "捷克" },
|
||||
{ code: "DK", name: "丹麦" },
|
||||
{ code: "DJ", name: "吉布提" },
|
||||
{ code: "DM", name: "多米尼克" },
|
||||
{ code: "DO", name: "多米尼加共和国" },
|
||||
{ code: "EC", name: "厄瓜多尔" },
|
||||
{ code: "EG", name: "埃及" },
|
||||
{ code: "SV", name: "萨尔瓦多" },
|
||||
{ code: "GQ", name: "赤道几内亚" },
|
||||
{ code: "ER", name: "厄立特里亚" },
|
||||
{ code: "EE", name: "爱沙尼亚" },
|
||||
{ code: "ET", name: "埃塞俄比亚" },
|
||||
{ code: "FK", name: "福克兰群岛" },
|
||||
{ code: "FO", name: "法罗群岛" },
|
||||
{ code: "FJ", name: "斐济" },
|
||||
{ code: "FI", name: "芬兰" },
|
||||
{ code: "FR", name: "法国" },
|
||||
{ code: "GF", name: "法属圭亚那" },
|
||||
{ code: "PF", name: "法属波利尼西亚" },
|
||||
{ code: "TF", name: "法属南部领地" },
|
||||
{ code: "GA", name: "加蓬" },
|
||||
{ code: "GM", name: "冈比亚" },
|
||||
{ code: "GE", name: "格鲁吉亚" },
|
||||
{ code: "DE", name: "德国" },
|
||||
{ code: "GH", name: "加纳" },
|
||||
{ code: "GI", name: "直布罗陀" },
|
||||
{ code: "GR", name: "希腊" },
|
||||
{ code: "GL", name: "格陵兰" },
|
||||
{ code: "GD", name: "格林纳达" },
|
||||
{ code: "GP", name: "瓜德罗普" },
|
||||
{ code: "GU", name: "关岛" },
|
||||
{ code: "GT", name: "危地马拉" },
|
||||
{ code: "GG", name: "根西岛" },
|
||||
{ code: "GN", name: "几内亚" },
|
||||
{ code: "GW", name: "几内亚比绍" },
|
||||
{ code: "GY", name: "圭亚那" },
|
||||
{ code: "HT", name: "海地" },
|
||||
{ code: "HM", name: "赫德岛和麦克唐纳群岛" },
|
||||
{ code: "VA", name: "梵蒂冈" },
|
||||
{ code: "HN", name: "洪都拉斯" },
|
||||
{ code: "HK", name: "中国香港" },
|
||||
{ code: "HU", name: "匈牙利" },
|
||||
{ code: "IS", name: "冰岛" },
|
||||
{ code: "IN", name: "印度" },
|
||||
{ code: "ID", name: "印度尼西亚" },
|
||||
{ code: "IR", name: "伊朗" },
|
||||
{ code: "IQ", name: "伊拉克" },
|
||||
{ code: "IE", name: "爱尔兰" },
|
||||
{ code: "IM", name: "马恩岛" },
|
||||
{ code: "IL", name: "以色列" },
|
||||
{ code: "IT", name: "意大利" },
|
||||
{ code: "JM", name: "牙买加" },
|
||||
{ code: "JP", name: "日本" },
|
||||
{ code: "JE", name: "泽西岛" },
|
||||
{ code: "JO", name: "约旦" },
|
||||
{ code: "KZ", name: "哈萨克斯坦" },
|
||||
{ code: "KE", name: "肯尼亚" },
|
||||
{ code: "KI", name: "基里巴斯" },
|
||||
{ code: "KR", name: "韩国" },
|
||||
{ code: "KP", name: "朝鲜" },
|
||||
{ code: "KW", name: "科威特" },
|
||||
{ code: "KG", name: "吉尔吉斯斯坦" },
|
||||
{ code: "LA", name: "老挝" },
|
||||
{ code: "LV", name: "拉脱维亚" },
|
||||
{ code: "LB", name: "黎巴嫩" },
|
||||
{ code: "LS", name: "莱索托" },
|
||||
{ code: "LR", name: "利比里亚" },
|
||||
{ code: "LY", name: "利比亚" },
|
||||
{ code: "LI", name: "列支敦士登" },
|
||||
{ code: "LT", name: "立陶宛" },
|
||||
{ code: "LU", name: "卢森堡" },
|
||||
{ code: "MO", name: "中国澳门" },
|
||||
{ code: "MK", name: "北马其顿" },
|
||||
{ code: "MG", name: "马达加斯加" },
|
||||
{ code: "MW", name: "马拉维" },
|
||||
{ code: "MY", name: "马来西亚" },
|
||||
{ code: "MV", name: "马尔代夫" },
|
||||
{ code: "ML", name: "马里" },
|
||||
{ code: "MT", name: "马耳他" },
|
||||
{ code: "MH", name: "马绍尔群岛" },
|
||||
{ code: "MQ", name: "马提尼克" },
|
||||
{ code: "MR", name: "毛里塔尼亚" },
|
||||
{ code: "MU", name: "毛里求斯" },
|
||||
{ code: "YT", name: "马约特" },
|
||||
{ code: "MX", name: "墨西哥" },
|
||||
{ code: "FM", name: "密克罗尼西亚" },
|
||||
{ code: "MD", name: "摩尔多瓦" },
|
||||
{ code: "MC", name: "摩纳哥" },
|
||||
{ code: "MN", name: "蒙古" },
|
||||
{ code: "ME", name: "黑山" },
|
||||
{ code: "MS", name: "蒙特塞拉特" },
|
||||
{ code: "MA", name: "摩洛哥" },
|
||||
{ code: "MZ", name: "莫桑比克" },
|
||||
{ code: "MM", name: "缅甸" },
|
||||
{ code: "NA", name: "纳米比亚" },
|
||||
{ code: "NR", name: "瑙鲁" },
|
||||
{ code: "NP", name: "尼泊尔" },
|
||||
{ code: "NL", name: "荷兰" },
|
||||
{ code: "AN", name: "荷属安的列斯" },
|
||||
{ code: "NC", name: "新喀里多尼亚" },
|
||||
{ code: "NZ", name: "新西兰" },
|
||||
{ code: "NI", name: "尼加拉瓜" },
|
||||
{ code: "NE", name: "尼日尔" },
|
||||
{ code: "NG", name: "尼日利亚" },
|
||||
{ code: "NU", name: "纽埃" },
|
||||
{ code: "NF", name: "诺福克岛" },
|
||||
{ code: "MP", name: "北马里亚纳群岛" },
|
||||
{ code: "NO", name: "挪威" },
|
||||
{ code: "OM", name: "阿曼" },
|
||||
{ code: "PK", name: "巴基斯坦" },
|
||||
{ code: "PW", name: "帕劳" },
|
||||
{ code: "PS", name: "巴勒斯坦" },
|
||||
{ code: "PA", name: "巴拿马" },
|
||||
{ code: "PG", name: "巴布亚新几内亚" },
|
||||
{ code: "PY", name: "巴拉圭" },
|
||||
{ code: "PE", name: "秘鲁" },
|
||||
{ code: "PH", name: "菲律宾" },
|
||||
{ code: "PN", name: "皮特凯恩群岛" },
|
||||
{ code: "PL", name: "波兰" },
|
||||
{ code: "PT", name: "葡萄牙" },
|
||||
{ code: "PR", name: "波多黎各" },
|
||||
{ code: "QA", name: "卡塔尔" },
|
||||
{ code: "RE", name: "留尼汪" },
|
||||
{ code: "RO", name: "罗马尼亚" },
|
||||
{ code: "RU", name: "俄罗斯" },
|
||||
{ code: "RW", name: "卢旺达" },
|
||||
{ code: "BL", name: "圣巴泰勒米" },
|
||||
{ code: "SH", name: "圣赫勒拿" },
|
||||
{ code: "KN", name: "圣基茨和尼维斯" },
|
||||
{ code: "LC", name: "圣卢西亚" },
|
||||
{ code: "MF", name: "法属圣马丁" },
|
||||
{ code: "PM", name: "圣皮埃尔和密克隆" },
|
||||
{ code: "VC", name: "圣文森特和格林纳丁斯" },
|
||||
{ code: "WS", name: "萨摩亚" },
|
||||
{ code: "SM", name: "圣马力诺" },
|
||||
{ code: "ST", name: "圣多美和普林西比" },
|
||||
{ code: "SA", name: "沙特阿拉伯" },
|
||||
{ code: "SN", name: "塞内加尔" },
|
||||
{ code: "RS", name: "塞尔维亚" },
|
||||
{ code: "SC", name: "塞舌尔" },
|
||||
{ code: "SL", name: "塞拉利昂" },
|
||||
{ code: "SG", name: "新加坡" },
|
||||
{ code: "SK", name: "斯洛伐克" },
|
||||
{ code: "SI", name: "斯洛文尼亚" },
|
||||
{ code: "SB", name: "所罗门群岛" },
|
||||
{ code: "SO", name: "索马里" },
|
||||
{ code: "ZA", name: "南非" },
|
||||
{ code: "GS", name: "南乔治亚和南桑威奇群岛" },
|
||||
{ code: "ES", name: "西班牙" },
|
||||
{ code: "LK", name: "斯里兰卡" },
|
||||
{ code: "SD", name: "苏丹" },
|
||||
{ code: "SR", name: "苏里南" },
|
||||
{ code: "SJ", name: "斯瓦尔巴和扬马延" },
|
||||
{ code: "SZ", name: "斯威士兰" },
|
||||
{ code: "SE", name: "瑞典" },
|
||||
{ code: "CH", name: "瑞士" },
|
||||
{ code: "SY", name: "叙利亚" },
|
||||
{ code: "TW", name: "中国台湾" },
|
||||
{ code: "TJ", name: "塔吉克斯坦" },
|
||||
{ code: "TZ", name: "坦桑尼亚" },
|
||||
{ code: "TH", name: "泰国" },
|
||||
{ code: "TL", name: "东帝汶" },
|
||||
{ code: "TG", name: "多哥" },
|
||||
{ code: "TK", name: "托克劳" },
|
||||
{ code: "TO", name: "汤加" },
|
||||
{ code: "TT", name: "特立尼达和多巴哥" },
|
||||
{ code: "TN", name: "突尼斯" },
|
||||
{ code: "TR", name: "土耳其" },
|
||||
{ code: "TM", name: "土库曼斯坦" },
|
||||
{ code: "TC", name: "特克斯和凯科斯群岛" },
|
||||
{ code: "TV", name: "图瓦卢" },
|
||||
{ code: "UG", name: "乌干达" },
|
||||
{ code: "UA", name: "乌克兰" },
|
||||
{ code: "AE", name: "阿联酋" },
|
||||
{ code: "GB", name: "英国" },
|
||||
{ code: "US", name: "美国" },
|
||||
{ code: "UM", name: "美国本土外小岛屿" },
|
||||
{ code: "UY", name: "乌拉圭" },
|
||||
{ code: "UZ", name: "乌兹别克斯坦" },
|
||||
{ code: "VU", name: "瓦努阿图" },
|
||||
{ code: "VE", name: "委内瑞拉" },
|
||||
{ code: "VN", name: "越南" },
|
||||
{ code: "VG", name: "英属维尔京群岛" },
|
||||
{ code: "VI", name: "美属维尔京群岛" },
|
||||
{ code: "WF", name: "瓦利斯和富图纳" },
|
||||
{ code: "EH", name: "西撒哈拉" },
|
||||
{ code: "YE", name: "也门" },
|
||||
{ code: "ZM", name: "赞比亚" },
|
||||
{ code: "ZW", name: "津巴布韦" },
|
||||
];
|
||||
export const RU_COUNTRIES = generateCountriesList("ru");
|
||||
export const EN_COUNTRIES = generateCountriesList("en");
|
||||
export const ZH_COUNTRIES = generateCountriesList("zh");
|
||||
@@ -71,10 +71,8 @@ export const clearBlobAndGLTFCache = async (url: string) => {
|
||||
*/
|
||||
export const clearMediaTransitionCache = async (
|
||||
previousMediaId: string | number | null,
|
||||
newMediaId: string | number | null,
|
||||
newMediaType?: number
|
||||
) => {
|
||||
console.log(newMediaId, newMediaType);
|
||||
// Если переключаемся с/на 3D модель, очищаем весь кеш
|
||||
if (newMediaType === 6 || previousMediaId) {
|
||||
await clearAllGLTFCache();
|
||||
|
||||
1070
src/shared/modals/ArticleSelectOrCreateDialog/index.tsx
Normal file
@@ -2,3 +2,4 @@ export * from "./SelectArticleDialog";
|
||||
export * from "./SelectMediaDialog";
|
||||
export * from "./PreviewMediaDialog";
|
||||
export * from "./UploadMediaDialog";
|
||||
export * from "./ArticleSelectOrCreateDialog";
|
||||
|
||||
@@ -340,55 +340,63 @@ class CreateSightStore {
|
||||
|
||||
createLeftArticle = async () => {
|
||||
/* ... your existing logic to create a new left article (placeholder or DB) ... */
|
||||
const ruName = (this.sight.ru.name || "").trim();
|
||||
const enName = (this.sight.en.name || "").trim();
|
||||
const zhName = (this.sight.zh.name || "").trim();
|
||||
|
||||
// If all names are empty, skip defaulting and use empty headings
|
||||
const hasAnyName = !!(ruName || enName || zhName);
|
||||
|
||||
const response = await languageInstance("ru").post("/article", {
|
||||
heading: "Новая левая статья",
|
||||
body: "Заполните контентом",
|
||||
heading: hasAnyName ? ruName : "",
|
||||
body: "",
|
||||
});
|
||||
const newLeftArticleId = response.data.id;
|
||||
|
||||
await languageInstance("en").patch(`/article/${newLeftArticleId}`, {
|
||||
heading: "New Left Article",
|
||||
body: "Fill with content",
|
||||
heading: hasAnyName ? enName : "",
|
||||
body: "",
|
||||
});
|
||||
await languageInstance("zh").patch(`/article/${newLeftArticleId}`, {
|
||||
heading: "新的左侧文章",
|
||||
body: "填写内容",
|
||||
heading: hasAnyName ? zhName : "",
|
||||
body: "",
|
||||
});
|
||||
|
||||
runInAction(() => {
|
||||
this.sight.left_article = newLeftArticleId; // Store the actual ID
|
||||
this.sight.ru.left = {
|
||||
heading: "Новая левая статья",
|
||||
body: "Заполните контентом",
|
||||
heading: hasAnyName ? ruName : "",
|
||||
body: "",
|
||||
media: [],
|
||||
};
|
||||
this.sight.en.left = {
|
||||
heading: "New Left Article",
|
||||
body: "Fill with content",
|
||||
heading: hasAnyName ? enName : "",
|
||||
body: "",
|
||||
media: [],
|
||||
};
|
||||
this.sight.zh.left = {
|
||||
heading: "新的左侧文章",
|
||||
body: "填写内容",
|
||||
heading: hasAnyName ? zhName : "",
|
||||
body: "",
|
||||
media: [],
|
||||
};
|
||||
|
||||
articlesStore.articles.ru.push({
|
||||
id: newLeftArticleId,
|
||||
heading: "Новая левая статья",
|
||||
body: "Заполните контентом",
|
||||
service_name: "Новая левая статья",
|
||||
heading: hasAnyName ? ruName : "",
|
||||
body: "",
|
||||
service_name: hasAnyName ? ruName : "",
|
||||
});
|
||||
articlesStore.articles.en.push({
|
||||
id: newLeftArticleId,
|
||||
heading: "New Left Article",
|
||||
body: "Fill with content",
|
||||
service_name: "New Left Article",
|
||||
heading: hasAnyName ? enName : "",
|
||||
body: "",
|
||||
service_name: hasAnyName ? enName : "",
|
||||
});
|
||||
articlesStore.articles.zh.push({
|
||||
id: newLeftArticleId,
|
||||
heading: "新的左侧文章",
|
||||
body: "填写内容",
|
||||
service_name: "新的左侧文章",
|
||||
heading: hasAnyName ? zhName : "",
|
||||
body: "",
|
||||
service_name: hasAnyName ? zhName : "",
|
||||
});
|
||||
});
|
||||
return newLeftArticleId;
|
||||
|
||||
@@ -400,16 +400,36 @@ class EditSightStore {
|
||||
};
|
||||
|
||||
createLeftArticle = async () => {
|
||||
const ruName = (this.sight.ru.name || "").trim();
|
||||
const enName = (this.sight.en.name || "").trim();
|
||||
const zhName = (this.sight.zh.name || "").trim();
|
||||
const hasAnyName = !!(ruName || enName || zhName);
|
||||
|
||||
const response = await languageInstance("ru").post(`/article`, {
|
||||
heading: "",
|
||||
heading: hasAnyName ? ruName : "",
|
||||
body: "",
|
||||
});
|
||||
|
||||
this.sight.common.left_article = response.data.id;
|
||||
|
||||
this.sight.ru.left.heading = "";
|
||||
this.sight.en.left.heading = "";
|
||||
this.sight.zh.left.heading = "";
|
||||
await languageInstance("en").patch(
|
||||
`/article/${this.sight.common.left_article}`,
|
||||
{
|
||||
heading: hasAnyName ? enName : "",
|
||||
body: "",
|
||||
}
|
||||
);
|
||||
await languageInstance("zh").patch(
|
||||
`/article/${this.sight.common.left_article}`,
|
||||
{
|
||||
heading: hasAnyName ? zhName : "",
|
||||
body: "",
|
||||
}
|
||||
);
|
||||
|
||||
this.sight.ru.left.heading = hasAnyName ? ruName : "";
|
||||
this.sight.en.left.heading = hasAnyName ? enName : "";
|
||||
this.sight.zh.left.heading = hasAnyName ? zhName : "";
|
||||
this.sight.ru.left.body = "";
|
||||
this.sight.en.left.body = "";
|
||||
this.sight.zh.left.body = "";
|
||||
|
||||
@@ -2,6 +2,7 @@ import { makeAutoObservable, runInAction } from "mobx";
|
||||
import { authInstance } from "@shared";
|
||||
|
||||
export type Route = {
|
||||
route_name: string;
|
||||
carrier: string;
|
||||
carrier_id: number;
|
||||
center_latitude: number;
|
||||
@@ -97,6 +98,7 @@ class RouteStore {
|
||||
};
|
||||
|
||||
editRouteData = {
|
||||
route_name: "",
|
||||
carrier: "",
|
||||
carrier_id: 0,
|
||||
center_latitude: "",
|
||||
@@ -110,7 +112,7 @@ class RouteStore {
|
||||
route_sys_number: "",
|
||||
scale_max: 0,
|
||||
scale_min: 0,
|
||||
video_preview: "",
|
||||
video_preview: "" as string | undefined,
|
||||
};
|
||||
|
||||
setEditRouteData = (data: any) => {
|
||||
@@ -118,6 +120,9 @@ class RouteStore {
|
||||
};
|
||||
|
||||
editRoute = async (id: number) => {
|
||||
if (!this.editRouteData.video_preview) {
|
||||
delete this.editRouteData.video_preview;
|
||||
}
|
||||
const response = await authInstance.patch(`/route/${id}`, {
|
||||
...this.editRouteData,
|
||||
center_latitude: parseFloat(this.editRouteData.center_latitude),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useRef, useState, DragEvent, useEffect } from "react";
|
||||
import React, { useRef, DragEvent } from "react";
|
||||
import { Paper, Box, Typography, Button, Tooltip } from "@mui/material";
|
||||
import { X, Info, Plus } from "lucide-react"; // Assuming lucide-react for icons
|
||||
import { editSightStore } from "@shared";
|
||||
@@ -27,18 +27,9 @@ export const ImageUploadCard: React.FC<ImageUploadCardProps> = ({
|
||||
tooltipText,
|
||||
}) => {
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const [isDragOver, setIsDragOver] = useState(false);
|
||||
const { setFileToUpload } = editSightStore;
|
||||
|
||||
useEffect(() => {
|
||||
if (isDragOver) {
|
||||
console.log("isDragOver");
|
||||
}
|
||||
}, [isDragOver]);
|
||||
|
||||
// --- Click to select file ---
|
||||
const handleZoneClick = () => {
|
||||
// Trigger the hidden file input click
|
||||
fileInputRef.current?.click();
|
||||
};
|
||||
|
||||
@@ -68,19 +59,16 @@ export const ImageUploadCard: React.FC<ImageUploadCardProps> = ({
|
||||
const handleDragOver = (event: DragEvent<HTMLDivElement>) => {
|
||||
event.preventDefault(); // Crucial to allow a drop
|
||||
event.stopPropagation();
|
||||
setIsDragOver(true);
|
||||
};
|
||||
|
||||
const handleDragLeave = (event: DragEvent<HTMLDivElement>) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
setIsDragOver(false);
|
||||
};
|
||||
|
||||
const handleDrop = async (event: DragEvent<HTMLDivElement>) => {
|
||||
event.preventDefault(); // Crucial to allow a drop
|
||||
event.stopPropagation();
|
||||
setIsDragOver(false);
|
||||
|
||||
const files = event.dataTransfer.files;
|
||||
if (files && files.length > 0) {
|
||||
|
||||
@@ -48,21 +48,24 @@ export const Layout: React.FC<LayoutProps> = observer(({ children }) => {
|
||||
<Box sx={{ display: "flex" }}>
|
||||
<AppBar position="fixed" open={open}>
|
||||
<Toolbar className="flex justify-between">
|
||||
<IconButton
|
||||
color="inherit"
|
||||
aria-label="open drawer"
|
||||
onClick={handleDrawerOpen}
|
||||
edge="start"
|
||||
sx={[
|
||||
{
|
||||
marginRight: 5,
|
||||
},
|
||||
open && { display: "none" },
|
||||
]}
|
||||
>
|
||||
<Menu />
|
||||
</IconButton>
|
||||
<CitySelector />
|
||||
<div className="flex items-center">
|
||||
<IconButton
|
||||
color="inherit"
|
||||
aria-label="open drawer"
|
||||
onClick={handleDrawerOpen}
|
||||
edge="start"
|
||||
sx={[
|
||||
{
|
||||
marginRight: 5,
|
||||
},
|
||||
open && { display: "none" },
|
||||
]}
|
||||
>
|
||||
<Menu />
|
||||
</IconButton>
|
||||
<CitySelector />
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2 items-center">
|
||||
<div className="flex flex-col gap-1">
|
||||
{(() => {
|
||||
@@ -114,7 +117,7 @@ export const Layout: React.FC<LayoutProps> = observer(({ children }) => {
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src="/favicon_ship.png"
|
||||
src="/favicon_ship.svg"
|
||||
alt="logo"
|
||||
width={40}
|
||||
height={40}
|
||||
|
||||
@@ -38,7 +38,7 @@ export function MediaViewer({
|
||||
// Используем новый cache manager для очистки кеша
|
||||
clearMediaTransitionCache(
|
||||
previousMediaId,
|
||||
media?.id || null,
|
||||
|
||||
media?.media_type
|
||||
);
|
||||
|
||||
|
||||
@@ -37,8 +37,8 @@ import { useEffect, useState } from "react";
|
||||
|
||||
import { toast } from "react-toastify";
|
||||
|
||||
// Компонент предупреждающего окна (перенесен сюда)
|
||||
import { SaveWithoutCityAgree } from "@widgets";
|
||||
import { LinkedStations } from "@pages";
|
||||
|
||||
export const InformationTab = observer(
|
||||
({ value, index }: { value: number; index: number }) => {
|
||||
@@ -62,7 +62,7 @@ export const InformationTab = observer(
|
||||
"thumbnail" | "watermark_lu" | "watermark_rd" | "video_preview" | null
|
||||
>(null);
|
||||
const { cities } = cityStore;
|
||||
// НОВОЕ СОСТОЯНИЕ ДЛЯ ПРЕДУПРЕЖДАЮЩЕГО ОКНА
|
||||
|
||||
const [isSaveWarningOpen, setIsSaveWarningOpen] = useState(false);
|
||||
|
||||
useEffect(() => {}, [hardcodeType]);
|
||||
@@ -119,16 +119,14 @@ export const InformationTab = observer(
|
||||
updateSightInfo(language, content, common);
|
||||
};
|
||||
|
||||
// НОВАЯ ФУНКЦИЯ: Фактическое сохранение (вызывается после подтверждения)
|
||||
const executeSave = async () => {
|
||||
await updateSight();
|
||||
toast.success("Достопримечательность сохранена");
|
||||
};
|
||||
|
||||
// ОБНОВЛЕННАЯ ФУНКЦИЯ: Проверка и вызов окна или сохранения
|
||||
const handleSave = async () => {
|
||||
const isCityMissing = !sight.common.city_id;
|
||||
// Проверяем названия на всех языках
|
||||
|
||||
const isNameMissing = !sight.ru.name || !sight.en.name || !sight.zh.name;
|
||||
|
||||
if (isCityMissing || isNameMissing) {
|
||||
@@ -139,13 +137,11 @@ export const InformationTab = observer(
|
||||
await executeSave();
|
||||
};
|
||||
|
||||
// Обработчик "Да" в предупреждающем окне
|
||||
const handleConfirmSave = async () => {
|
||||
setIsSaveWarningOpen(false);
|
||||
await executeSave();
|
||||
};
|
||||
|
||||
// Обработчик "Нет" в предупреждающем окне
|
||||
const handleCancelSave = () => {
|
||||
setIsSaveWarningOpen(false);
|
||||
};
|
||||
@@ -275,6 +271,16 @@ export const InformationTab = observer(
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ width: "80%" }}>
|
||||
{sight.common.id !== 0 && (
|
||||
<LinkedStations
|
||||
parentId={sight.common.id}
|
||||
fields={[{ label: "Название", data: "name" }]}
|
||||
type="edit"
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
@@ -431,7 +437,7 @@ export const InformationTab = observer(
|
||||
variant="contained"
|
||||
color="success"
|
||||
startIcon={<Save color="white" size={18} />}
|
||||
onClick={handleSave} // Используем новую функцию-обработчик
|
||||
onClick={handleSave}
|
||||
>
|
||||
Сохранить
|
||||
</Button>
|
||||
@@ -538,7 +544,6 @@ export const InformationTab = observer(
|
||||
</Dialog>
|
||||
)}
|
||||
|
||||
{/* ИНТЕГРИРОВАННОЕ ПРЕДУПРЕЖДАЮЩЕЕ ОКНО */}
|
||||
{isSaveWarningOpen && (
|
||||
<SaveWithoutCityAgree
|
||||
blocker={{
|
||||
@@ -550,4 +555,4 @@ export const InformationTab = observer(
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
);
|
||||
|
||||
@@ -11,6 +11,7 @@ interface VideoPreviewCardProps {
|
||||
onDeleteVideoClick: () => void;
|
||||
onSelectVideoClick: (file?: File) => void;
|
||||
tooltipText?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const VideoPreviewCard: React.FC<VideoPreviewCardProps> = ({
|
||||
@@ -20,6 +21,7 @@ export const VideoPreviewCard: React.FC<VideoPreviewCardProps> = ({
|
||||
onDeleteVideoClick,
|
||||
onSelectVideoClick,
|
||||
tooltipText,
|
||||
className,
|
||||
}) => {
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const [isDragOver, setIsDragOver] = useState(false);
|
||||
@@ -89,7 +91,10 @@ export const VideoPreviewCard: React.FC<VideoPreviewCardProps> = ({
|
||||
gap: 1,
|
||||
flex: 1,
|
||||
minWidth: 150,
|
||||
width: "min-content",
|
||||
mx: "auto",
|
||||
}}
|
||||
className={className}
|
||||
>
|
||||
<Box sx={{ display: "flex", alignItems: "center" }}>
|
||||
<Typography variant="subtitle2" gutterBottom sx={{ mb: 0, mr: 0.5 }}>
|
||||
@@ -127,7 +132,10 @@ export const VideoPreviewCard: React.FC<VideoPreviewCardProps> = ({
|
||||
</button>
|
||||
)}
|
||||
{videoId ? (
|
||||
<Box sx={{ position: "relative", width: "100%", height: "100%" }}>
|
||||
<Box
|
||||
sx={{ position: "relative", width: "100%", height: "100%" }}
|
||||
className={className}
|
||||
>
|
||||
<video
|
||||
src={`${
|
||||
import.meta.env.VITE_KRBL_MEDIA
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { defineConfig } from "vite";
|
||||
import { defineConfig, type UserConfigExport } from "vite";
|
||||
import react from "@vitejs/plugin-react";
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
import path from "path";
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react(), tailwindcss()],
|
||||
plugins: [
|
||||
react(),
|
||||
tailwindcss(),
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
"@shared": path.resolve(__dirname, "src/shared"),
|
||||
@@ -16,4 +18,9 @@ export default defineConfig({
|
||||
"@app": path.resolve(__dirname, "src/app"),
|
||||
},
|
||||
},
|
||||
|
||||
build: {
|
||||
chunkSizeWarningLimit: 5000,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||