Compare commits
8 Commits
main
...
1917b2cf5a
| Author | SHA1 | Date | |
|---|---|---|---|
| 1917b2cf5a | |||
| 5298fb9f60 | |||
| c95a6517e9 | |||
| 79f523e9cb | |||
| 90f3d66b22 | |||
| 2b48ade2f1 | |||
| b0fdf03cc6 | |||
|
|
349c7009c6 |
@@ -2,7 +2,7 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<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" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Белые ночи</title>
|
<title>Белые ночи</title>
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
3409
package-lock.json
generated
@@ -3,6 +3,7 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
"license": "UNLICENSED",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "tsc -b && vite build",
|
"build": "tsc -b && vite build",
|
||||||
@@ -23,6 +24,7 @@
|
|||||||
"@tailwindcss/vite": "^4.1.8",
|
"@tailwindcss/vite": "^4.1.8",
|
||||||
"axios": "^1.9.0",
|
"axios": "^1.9.0",
|
||||||
"easymde": "^2.20.0",
|
"easymde": "^2.20.0",
|
||||||
|
"i18n-iso-countries": "^7.14.0",
|
||||||
"lucide-react": "^0.511.0",
|
"lucide-react": "^0.511.0",
|
||||||
"mobx": "^6.13.7",
|
"mobx": "^6.13.7",
|
||||||
"mobx-react-lite": "^4.1.0",
|
"mobx-react-lite": "^4.1.0",
|
||||||
@@ -30,11 +32,10 @@
|
|||||||
"path": "^0.12.7",
|
"path": "^0.12.7",
|
||||||
"pixi.js": "^8.10.1",
|
"pixi.js": "^8.10.1",
|
||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
"react-colorful": "^5.6.1",
|
|
||||||
"react-dom": "^19.1.0",
|
"react-dom": "^19.1.0",
|
||||||
"react-dropzone": "^14.3.8",
|
|
||||||
"react-markdown": "^10.1.0",
|
"react-markdown": "^10.1.0",
|
||||||
"react-photo-sphere-viewer": "^6.2.3",
|
"react-photo-sphere-viewer": "^6.2.3",
|
||||||
|
"react-router": "^7.9.4",
|
||||||
"react-router-dom": "^7.6.1",
|
"react-router-dom": "^7.6.1",
|
||||||
"react-simplemde-editor": "^5.2.0",
|
"react-simplemde-editor": "^5.2.0",
|
||||||
"react-toastify": "^11.0.5",
|
"react-toastify": "^11.0.5",
|
||||||
@@ -53,9 +54,11 @@
|
|||||||
"eslint-plugin-react-hooks": "^5.2.0",
|
"eslint-plugin-react-hooks": "^5.2.0",
|
||||||
"eslint-plugin-react-refresh": "^0.4.19",
|
"eslint-plugin-react-refresh": "^0.4.19",
|
||||||
"globals": "^16.0.0",
|
"globals": "^16.0.0",
|
||||||
|
"rollup-plugin-visualizer": "^6.0.5",
|
||||||
"typescript": "~5.8.3",
|
"typescript": "~5.8.3",
|
||||||
"typescript-eslint": "^8.30.1",
|
"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"
|
"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 |
@@ -16,12 +16,7 @@ import {
|
|||||||
SnapshotListPage,
|
SnapshotListPage,
|
||||||
CarrierListPage,
|
CarrierListPage,
|
||||||
StationListPage,
|
StationListPage,
|
||||||
// VehicleListPage,
|
|
||||||
ArticleListPage,
|
ArticleListPage,
|
||||||
|
|
||||||
// CountryPreviewPage,
|
|
||||||
// VehiclePreviewPage,
|
|
||||||
// CarrierPreviewPage,
|
|
||||||
SnapshotCreatePage,
|
SnapshotCreatePage,
|
||||||
CountryCreatePage,
|
CountryCreatePage,
|
||||||
CityCreatePage,
|
CityCreatePage,
|
||||||
@@ -31,7 +26,6 @@ import {
|
|||||||
CityEditPage,
|
CityEditPage,
|
||||||
UserCreatePage,
|
UserCreatePage,
|
||||||
UserEditPage,
|
UserEditPage,
|
||||||
// VehicleEditPage,
|
|
||||||
CarrierEditPage,
|
CarrierEditPage,
|
||||||
StationCreatePage,
|
StationCreatePage,
|
||||||
StationPreviewPage,
|
StationPreviewPage,
|
||||||
@@ -75,7 +69,6 @@ const ProtectedRoute = ({ children }: { children: React.ReactNode }) => {
|
|||||||
return <>{children}</>;
|
return <>{children}</>;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Чтобы очистка сторов происходила при смене локации
|
|
||||||
const ClearStoresWrapper: React.FC<{ children: React.ReactNode }> = ({
|
const ClearStoresWrapper: React.FC<{ children: React.ReactNode }> = ({
|
||||||
children,
|
children,
|
||||||
}) => {
|
}) => {
|
||||||
@@ -116,65 +109,45 @@ const router = createBrowserRouter([
|
|||||||
children: [
|
children: [
|
||||||
{ index: true, element: <MainPage /> },
|
{ index: true, element: <MainPage /> },
|
||||||
|
|
||||||
// Sight
|
|
||||||
{ path: "sight", element: <SightListPage /> },
|
{ path: "sight", element: <SightListPage /> },
|
||||||
{ path: "sight/create", element: <CreateSightPage /> },
|
{ path: "sight/create", element: <CreateSightPage /> },
|
||||||
{ path: "sight/:id/edit", element: <EditSightPage /> },
|
{ path: "sight/:id/edit", element: <EditSightPage /> },
|
||||||
|
|
||||||
// Device
|
|
||||||
{ path: "devices", element: <DevicesPage /> },
|
{ path: "devices", element: <DevicesPage /> },
|
||||||
|
|
||||||
// Map
|
|
||||||
{ path: "map", element: <MapPage /> },
|
{ path: "map", element: <MapPage /> },
|
||||||
|
|
||||||
// Media
|
|
||||||
{ path: "media", element: <MediaListPage /> },
|
{ path: "media", element: <MediaListPage /> },
|
||||||
{ path: "media/:id", element: <MediaPreviewPage /> },
|
{ path: "media/:id", element: <MediaPreviewPage /> },
|
||||||
{ path: "media/:id/edit", element: <MediaEditPage /> },
|
{ path: "media/:id/edit", element: <MediaEditPage /> },
|
||||||
|
|
||||||
// Country
|
|
||||||
{ path: "country", element: <CountryListPage /> },
|
{ path: "country", element: <CountryListPage /> },
|
||||||
{ path: "country/create", element: <CountryCreatePage /> },
|
{ path: "country/create", element: <CountryCreatePage /> },
|
||||||
{ path: "country/add", element: <CountryAddPage /> },
|
{ path: "country/add", element: <CountryAddPage /> },
|
||||||
// { path: "country/:id", element: <CountryPreviewPage /> },
|
|
||||||
{ path: "country/:id/edit", element: <CountryEditPage /> },
|
{ path: "country/:id/edit", element: <CountryEditPage /> },
|
||||||
// City
|
|
||||||
{ path: "city", element: <CityListPage /> },
|
{ path: "city", element: <CityListPage /> },
|
||||||
{ path: "city/create", element: <CityCreatePage /> },
|
{ path: "city/create", element: <CityCreatePage /> },
|
||||||
// { path: "city/:id", element: <CityPreviewPage /> },
|
|
||||||
{ path: "city/:id/edit", element: <CityEditPage /> },
|
{ path: "city/:id/edit", element: <CityEditPage /> },
|
||||||
// Route
|
|
||||||
{ path: "route", element: <RouteListPage /> },
|
{ path: "route", element: <RouteListPage /> },
|
||||||
{ path: "route/create", element: <RouteCreatePage /> },
|
{ path: "route/create", element: <RouteCreatePage /> },
|
||||||
{ path: "route/:id/edit", element: <RouteEditPage /> },
|
{ path: "route/:id/edit", element: <RouteEditPage /> },
|
||||||
|
|
||||||
// User
|
|
||||||
{ path: "user", element: <UserListPage /> },
|
{ path: "user", element: <UserListPage /> },
|
||||||
{ path: "user/create", element: <UserCreatePage /> },
|
{ path: "user/create", element: <UserCreatePage /> },
|
||||||
{ path: "user/:id/edit", element: <UserEditPage /> },
|
{ path: "user/:id/edit", element: <UserEditPage /> },
|
||||||
// Snapshot
|
|
||||||
{ path: "snapshot", element: <SnapshotListPage /> },
|
{ path: "snapshot", element: <SnapshotListPage /> },
|
||||||
{ path: "snapshot/create", element: <SnapshotCreatePage /> },
|
{ path: "snapshot/create", element: <SnapshotCreatePage /> },
|
||||||
|
|
||||||
// Carrier
|
|
||||||
{ path: "carrier", element: <CarrierListPage /> },
|
{ path: "carrier", element: <CarrierListPage /> },
|
||||||
{ path: "carrier/create", element: <CarrierCreatePage /> },
|
{ path: "carrier/create", element: <CarrierCreatePage /> },
|
||||||
// { path: "carrier/:id", element: <CarrierPreviewPage /> },
|
|
||||||
{ path: "carrier/:id/edit", element: <CarrierEditPage /> },
|
{ path: "carrier/:id/edit", element: <CarrierEditPage /> },
|
||||||
// Station
|
|
||||||
{ path: "station", element: <StationListPage /> },
|
{ path: "station", element: <StationListPage /> },
|
||||||
{ path: "station/create", element: <StationCreatePage /> },
|
{ path: "station/create", element: <StationCreatePage /> },
|
||||||
{ path: "station/:id", element: <StationPreviewPage /> },
|
{ path: "station/:id", element: <StationPreviewPage /> },
|
||||||
{ path: "station/:id/edit", element: <StationEditPage /> },
|
{ path: "station/:id/edit", element: <StationEditPage /> },
|
||||||
// Vehicle
|
|
||||||
// { path: "vehicle", element: <VehicleListPage /> },
|
|
||||||
{ path: "vehicle/create", element: <VehicleCreatePage /> },
|
{ path: "vehicle/create", element: <VehicleCreatePage /> },
|
||||||
// { path: "vehicle/:id", element: <VehiclePreviewPage /> },
|
|
||||||
// { path: "vehicle/:id/edit", element: <VehicleEditPage /> },
|
|
||||||
// Article
|
|
||||||
{ path: "article", element: <ArticleListPage /> },
|
{ path: "article", element: <ArticleListPage /> },
|
||||||
{ path: "article/:id", element: <ArticlePreviewPage /> },
|
{ path: "article/:id", element: <ArticlePreviewPage /> },
|
||||||
// { path: "media/create", element: <CreateMediaPage /> },
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -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";
|
@import "tailwindcss";
|
||||||
@plugin "@tailwindcss/typography";
|
@plugin "@tailwindcss/typography";
|
||||||
|
|
||||||
button {
|
button {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,7 +48,6 @@ export const CarrierCreatePage = observer(() => {
|
|||||||
languageStore.setLanguage("ru");
|
languageStore.setLanguage("ru");
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Автоматически устанавливаем выбранный город при загрузке страницы
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedCityId && !createCarrierData.city_id) {
|
if (selectedCityId && !createCarrierData.city_id) {
|
||||||
setCreateCarrierData(
|
setCreateCarrierData(
|
||||||
|
|||||||
@@ -74,7 +74,6 @@ export const CarrierEditPage = observer(() => {
|
|||||||
mediaStore.getMedia();
|
mediaStore.getMedia();
|
||||||
})();
|
})();
|
||||||
|
|
||||||
// Устанавливаем русский язык при загрузке страницы
|
|
||||||
languageStore.setLanguage("ru");
|
languageStore.setLanguage("ru");
|
||||||
}, [id]);
|
}, [id]);
|
||||||
|
|
||||||
|
|||||||
@@ -44,7 +44,6 @@ export const CityEditPage = observer(() => {
|
|||||||
const { getMedia, getOneMedia } = mediaStore;
|
const { getMedia, getOneMedia } = mediaStore;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Устанавливаем русский язык при загрузке страницы
|
|
||||||
languageStore.setLanguage("ru");
|
languageStore.setLanguage("ru");
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -64,12 +63,11 @@ export const CityEditPage = observer(() => {
|
|||||||
(async () => {
|
(async () => {
|
||||||
if (id) {
|
if (id) {
|
||||||
await getCountries("ru");
|
await getCountries("ru");
|
||||||
// Fetch data for all languages
|
|
||||||
const ruData = await getCity(id as string, "ru");
|
const ruData = await getCity(id as string, "ru");
|
||||||
const enData = await getCity(id as string, "en");
|
const enData = await getCity(id as string, "en");
|
||||||
const zhData = await getCity(id as string, "zh");
|
const zhData = await getCity(id as string, "zh");
|
||||||
|
|
||||||
// Set data for each language
|
|
||||||
setEditCityData(ruData.name, ruData.country_code, ruData.arms, "ru");
|
setEditCityData(ruData.name, ruData.country_code, ruData.arms, "ru");
|
||||||
setEditCityData(enData.name, enData.country_code, enData.arms, "en");
|
setEditCityData(enData.name, enData.country_code, enData.arms, "en");
|
||||||
setEditCityData(zhData.name, zhData.country_code, zhData.arms, "zh");
|
setEditCityData(zhData.name, zhData.country_code, zhData.arms, "zh");
|
||||||
@@ -207,7 +205,7 @@ export const CityEditPage = observer(() => {
|
|||||||
open={isSelectMediaOpen}
|
open={isSelectMediaOpen}
|
||||||
onClose={() => setIsSelectMediaOpen(false)}
|
onClose={() => setIsSelectMediaOpen(false)}
|
||||||
onSelectMedia={handleMediaSelect}
|
onSelectMedia={handleMediaSelect}
|
||||||
mediaType={1} // Тип медиа для иконок
|
mediaType={1}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<UploadMediaDialog
|
<UploadMediaDialog
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ export const CountryEditPage = observer(() => {
|
|||||||
countryStore;
|
countryStore;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Устанавливаем русский язык при загрузке страницы
|
|
||||||
languageStore.setLanguage("ru");
|
languageStore.setLanguage("ru");
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -36,12 +35,10 @@ export const CountryEditPage = observer(() => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
if (id) {
|
if (id) {
|
||||||
// Fetch data for all languages
|
|
||||||
const ruData = await getCountry(id as string, "ru");
|
const ruData = await getCountry(id as string, "ru");
|
||||||
const enData = await getCountry(id as string, "en");
|
const enData = await getCountry(id as string, "en");
|
||||||
const zhData = await getCountry(id as string, "zh");
|
const zhData = await getCountry(id as string, "zh");
|
||||||
|
|
||||||
// Set data for each language
|
|
||||||
setEditCountryData(ruData.name, "ru");
|
setEditCountryData(ruData.name, "ru");
|
||||||
setEditCountryData(enData.name, "en");
|
setEditCountryData(enData.name, "en");
|
||||||
setEditCountryData(zhData.name, "zh");
|
setEditCountryData(zhData.name, "zh");
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ export const LoginPage = () => {
|
|||||||
const { login } = authStore;
|
const { login } = authStore;
|
||||||
const { getUsers } = userStore;
|
const { getUsers } = userStore;
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Load saved credentials if they exist
|
|
||||||
const savedEmail = localStorage.getItem("rememberedEmail");
|
const savedEmail = localStorage.getItem("rememberedEmail");
|
||||||
const savedPassword = localStorage.getItem("rememberedPassword");
|
const savedPassword = localStorage.getItem("rememberedPassword");
|
||||||
if (savedEmail && savedPassword) {
|
if (savedEmail && savedPassword) {
|
||||||
@@ -42,7 +41,6 @@ export const LoginPage = () => {
|
|||||||
try {
|
try {
|
||||||
await login(email, password);
|
await login(email, password);
|
||||||
|
|
||||||
// Save or clear credentials based on remember me checkbox
|
|
||||||
if (rememberMe) {
|
if (rememberMe) {
|
||||||
localStorage.setItem("rememberedEmail", email);
|
localStorage.setItem("rememberedEmail", email);
|
||||||
localStorage.setItem("rememberedPassword", password);
|
localStorage.setItem("rememberedPassword", password);
|
||||||
|
|||||||
@@ -22,10 +22,8 @@ interface ApiSight {
|
|||||||
longitude: number;
|
longitude: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Допуск для сравнения координат, чтобы избежать ошибок с точностью чисел.
|
|
||||||
const COORDINATE_PRECISION_TOLERANCE = 1e-9;
|
const COORDINATE_PRECISION_TOLERANCE = 1e-9;
|
||||||
|
|
||||||
// Вспомогательная функция, обновленная для сравнения с допуском.
|
|
||||||
const arePathsEqual = (
|
const arePathsEqual = (
|
||||||
path1: [number, number][],
|
path1: [number, number][],
|
||||||
path2: [number, number][]
|
path2: [number, number][]
|
||||||
@@ -136,7 +134,6 @@ class MapStore {
|
|||||||
longitude: geometry.coordinates[0],
|
longitude: geometry.coordinates[0],
|
||||||
};
|
};
|
||||||
|
|
||||||
// ИЗМЕНЕНИЕ: Сравнение координат с допуском
|
|
||||||
if (
|
if (
|
||||||
originalStation.name !== currentStation.name ||
|
originalStation.name !== currentStation.name ||
|
||||||
Math.abs(originalStation.latitude - currentStation.latitude) >
|
Math.abs(originalStation.latitude - currentStation.latitude) >
|
||||||
@@ -155,7 +152,6 @@ class MapStore {
|
|||||||
path: geometry.coordinates,
|
path: geometry.coordinates,
|
||||||
};
|
};
|
||||||
|
|
||||||
// ИЗМЕНЕНИЕ: Используется новая функция arePathsEqual с допуском
|
|
||||||
if (
|
if (
|
||||||
originalRoute.route_number !== currentRoute.route_number ||
|
originalRoute.route_number !== currentRoute.route_number ||
|
||||||
!arePathsEqual(originalRoute.path, currentRoute.path)
|
!arePathsEqual(originalRoute.path, currentRoute.path)
|
||||||
@@ -173,7 +169,6 @@ class MapStore {
|
|||||||
longitude: geometry.coordinates[0],
|
longitude: geometry.coordinates[0],
|
||||||
};
|
};
|
||||||
|
|
||||||
// ИЗМЕНЕНИЕ: Сравнение координат с допуском
|
|
||||||
if (
|
if (
|
||||||
originalSight.name !== currentSight.name ||
|
originalSight.name !== currentSight.name ||
|
||||||
originalSight.description !== currentSight.description ||
|
originalSight.description !== currentSight.description ||
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ export const MediaEditPage = observer(() => {
|
|||||||
|
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
const [newFile, setNewFile] = useState<File | null>(null);
|
const [newFile, setNewFile] = useState<File | null>(null);
|
||||||
const [uploadDialogOpen, setUploadDialogOpen] = useState(false); // State for the upload dialog
|
const [uploadDialogOpen, setUploadDialogOpen] = useState(false);
|
||||||
|
|
||||||
const media = id ? mediaStore.media.find((m) => m.id === id) : null;
|
const media = id ? mediaStore.media.find((m) => m.id === id) : null;
|
||||||
const [mediaName, setMediaName] = useState(media?.media_name ?? "");
|
const [mediaName, setMediaName] = useState(media?.media_name ?? "");
|
||||||
@@ -55,51 +55,27 @@ export const MediaEditPage = observer(() => {
|
|||||||
setMediaFilename(media.filename);
|
setMediaFilename(media.filename);
|
||||||
setMediaType(media.media_type);
|
setMediaType(media.media_type);
|
||||||
|
|
||||||
// Set available media types based on current file extension
|
|
||||||
const extension = media.filename.split(".").pop()?.toLowerCase();
|
const extension = media.filename.split(".").pop()?.toLowerCase();
|
||||||
if (extension) {
|
if (extension) {
|
||||||
if (["glb", "gltf"].includes(extension)) {
|
if (["glb", "gltf"].includes(extension)) {
|
||||||
setAvailableMediaTypes([6]); // 3D model
|
setAvailableMediaTypes([6]);
|
||||||
} else if (
|
} else if (
|
||||||
["jpg", "jpeg", "png", "gif", "svg", "webp", "bmp"].includes(
|
["jpg", "jpeg", "png", "gif", "svg", "webp", "bmp"].includes(
|
||||||
extension
|
extension
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
setAvailableMediaTypes([1, 3, 4, 5]); // Photo, Icon, Watermark, Panorama
|
setAvailableMediaTypes([1, 3, 4, 5]);
|
||||||
} else if (["mp4", "webm", "mov"].includes(extension)) {
|
} else if (["mp4", "webm", "mov"].includes(extension)) {
|
||||||
setAvailableMediaTypes([2]); // Video
|
setAvailableMediaTypes([2]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [media]);
|
}, [media]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Устанавливаем русский язык при загрузке страницы
|
|
||||||
languageStore.setLanguage("ru");
|
languageStore.setLanguage("ru");
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// const handleDrop = (e: DragEvent<HTMLDivElement>) => {
|
|
||||||
// e.preventDefault();
|
|
||||||
// e.stopPropagation();
|
|
||||||
// setIsDragging(false);
|
|
||||||
|
|
||||||
// const files = Array.from(e.dataTransfer.files);
|
|
||||||
// if (files.length > 0) {
|
|
||||||
// setNewFile(files[0]);
|
|
||||||
// setMediaFilename(files[0].name);
|
|
||||||
// setUploadDialogOpen(true); // Open dialog on file drop
|
|
||||||
// }
|
|
||||||
// };
|
|
||||||
|
|
||||||
// const handleDragOver = (e: DragEvent<HTMLDivElement>) => {
|
|
||||||
// e.preventDefault();
|
|
||||||
// setIsDragging(true);
|
|
||||||
// };
|
|
||||||
|
|
||||||
// const handleDragLeave = () => {
|
|
||||||
// setIsDragging(false);
|
|
||||||
// };
|
|
||||||
|
|
||||||
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const files = e.target.files;
|
const files = e.target.files;
|
||||||
if (files && files.length > 0) {
|
if (files && files.length > 0) {
|
||||||
@@ -107,26 +83,25 @@ export const MediaEditPage = observer(() => {
|
|||||||
setNewFile(file);
|
setNewFile(file);
|
||||||
setMediaFilename(file.name);
|
setMediaFilename(file.name);
|
||||||
|
|
||||||
// Determine media type based on file extension
|
|
||||||
const extension = file.name.split(".").pop()?.toLowerCase();
|
const extension = file.name.split(".").pop()?.toLowerCase();
|
||||||
if (extension) {
|
if (extension) {
|
||||||
if (["glb", "gltf"].includes(extension)) {
|
if (["glb", "gltf"].includes(extension)) {
|
||||||
setAvailableMediaTypes([6]); // 3D model
|
setAvailableMediaTypes([6]);
|
||||||
setMediaType(6);
|
setMediaType(6);
|
||||||
} else if (
|
} else if (
|
||||||
["jpg", "jpeg", "png", "gif", "svg", "webp", "bmp"].includes(
|
["jpg", "jpeg", "png", "gif", "svg", "webp", "bmp"].includes(
|
||||||
extension
|
extension
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
setAvailableMediaTypes([1, 3, 4, 5]); // Photo, Icon, Watermark, Panorama
|
setAvailableMediaTypes([1, 3, 4, 5]);
|
||||||
setMediaType(1); // Default to Photo
|
setMediaType(1);
|
||||||
} else if (["mp4", "webm", "mov"].includes(extension)) {
|
} else if (["mp4", "webm", "mov"].includes(extension)) {
|
||||||
setAvailableMediaTypes([2]); // Video
|
setAvailableMediaTypes([2]);
|
||||||
setMediaType(2);
|
setMediaType(2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setUploadDialogOpen(true); // Open dialog on file selection
|
setUploadDialogOpen(true);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -143,11 +118,6 @@ export const MediaEditPage = observer(() => {
|
|||||||
type: mediaType,
|
type: mediaType,
|
||||||
});
|
});
|
||||||
|
|
||||||
// If a new file was selected, the actual file upload will happen
|
|
||||||
// via the UploadMediaDialog. We just need to make sure the metadata
|
|
||||||
// is updated correctly before or after.
|
|
||||||
// Since the dialog handles the actual upload, we don't call updateMediaFile here.
|
|
||||||
|
|
||||||
setSuccess(true);
|
setSuccess(true);
|
||||||
handleUploadSuccess();
|
handleUploadSuccess();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -158,17 +128,15 @@ export const MediaEditPage = observer(() => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleUploadSuccess = () => {
|
const handleUploadSuccess = () => {
|
||||||
// After successful upload in the dialog, refresh media data if needed
|
|
||||||
if (id) {
|
if (id) {
|
||||||
mediaStore.getOneMedia(id);
|
mediaStore.getOneMedia(id);
|
||||||
}
|
}
|
||||||
setNewFile(null); // Clear the new file state after successful upload
|
setNewFile(null);
|
||||||
setUploadDialogOpen(false);
|
setUploadDialogOpen(false);
|
||||||
setSuccess(true);
|
setSuccess(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!media && id) {
|
if (!media && id) {
|
||||||
// Only show loading if an ID is present and media is not yet loaded
|
|
||||||
return (
|
return (
|
||||||
<Box className="flex justify-center items-center h-screen">
|
<Box className="flex justify-center items-center h-screen">
|
||||||
<CircularProgress />
|
<CircularProgress />
|
||||||
|
|||||||
@@ -42,7 +42,6 @@ import {
|
|||||||
} from "@shared";
|
} from "@shared";
|
||||||
import { EditStationModal } from "../../widgets/modals/EditStationModal";
|
import { EditStationModal } from "../../widgets/modals/EditStationModal";
|
||||||
|
|
||||||
// Helper function to insert an item at a specific position (1-based index)
|
|
||||||
function insertAtPosition<T>(arr: T[], pos: number, value: T): T[] {
|
function insertAtPosition<T>(arr: T[], pos: number, value: T): T[] {
|
||||||
const index = pos - 1;
|
const index = pos - 1;
|
||||||
const result = [...arr];
|
const result = [...arr];
|
||||||
@@ -54,7 +53,6 @@ function insertAtPosition<T>(arr: T[], pos: number, value: T): T[] {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper function to reorder items after drag and drop
|
|
||||||
const reorder = <T,>(list: T[], startIndex: number, endIndex: number): T[] => {
|
const reorder = <T,>(list: T[], startIndex: number, endIndex: number): T[] => {
|
||||||
const result = Array.from(list);
|
const result = Array.from(list);
|
||||||
const [removed] = result.splice(startIndex, 1);
|
const [removed] = result.splice(startIndex, 1);
|
||||||
@@ -152,13 +150,11 @@ const LinkedItemsContentsInner = <
|
|||||||
const availableItems = allItems
|
const availableItems = allItems
|
||||||
.filter((item) => !linkedItems.some((linked) => linked.id === item.id))
|
.filter((item) => !linkedItems.some((linked) => linked.id === item.id))
|
||||||
.filter((item) => {
|
.filter((item) => {
|
||||||
// Если направление маршрута не указано, показываем все станции
|
|
||||||
if (routeDirection === undefined) return true;
|
if (routeDirection === undefined) return true;
|
||||||
// Фильтруем станции по направлению маршрута
|
|
||||||
return item.direction === routeDirection;
|
return item.direction === routeDirection;
|
||||||
})
|
})
|
||||||
.filter((item) => {
|
.filter((item) => {
|
||||||
// Фильтруем по городу из навбара
|
|
||||||
const selectedCityId = selectedCityStore.selectedCityId;
|
const selectedCityId = selectedCityStore.selectedCityId;
|
||||||
if (selectedCityId && "city_id" in item) {
|
if (selectedCityId && "city_id" in item) {
|
||||||
return item.city_id === selectedCityId;
|
return item.city_id === selectedCityId;
|
||||||
@@ -167,7 +163,6 @@ const LinkedItemsContentsInner = <
|
|||||||
})
|
})
|
||||||
.sort((a, b) => a.name.localeCompare(b.name));
|
.sort((a, b) => a.name.localeCompare(b.name));
|
||||||
|
|
||||||
// Фильтрация по поиску для массового режима
|
|
||||||
const filteredAvailableItems = availableItems.filter((item) => {
|
const filteredAvailableItems = availableItems.filter((item) => {
|
||||||
if (!searchQuery.trim()) return true;
|
if (!searchQuery.trim()) return true;
|
||||||
return String(item.name).toLowerCase().includes(searchQuery.toLowerCase());
|
return String(item.name).toLowerCase().includes(searchQuery.toLowerCase());
|
||||||
@@ -562,7 +557,14 @@ const LinkedItemsContentsInner = <
|
|||||||
size="small"
|
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={{
|
sx={{
|
||||||
margin: 0,
|
margin: 0,
|
||||||
"& .MuiFormControlLabel-label": {
|
"& .MuiFormControlLabel-label": {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
DialogContent,
|
DialogContent,
|
||||||
DialogActions,
|
DialogActions,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import { MediaViewer } from "@widgets";
|
import { MediaViewer, VideoPreviewCard } from "@widgets";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { ArrowLeft, Loader2, Save, Plus } from "lucide-react";
|
import { ArrowLeft, Loader2, Save, Plus } from "lucide-react";
|
||||||
import { useEffect, useState, useMemo } from "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 { Route, routeStore } from "../../../shared/store/RouteStore";
|
||||||
import {
|
import {
|
||||||
languageStore,
|
languageStore,
|
||||||
SelectArticleModal,
|
ArticleSelectOrCreateDialog,
|
||||||
SelectMediaDialog,
|
SelectMediaDialog,
|
||||||
selectedCityStore,
|
selectedCityStore,
|
||||||
|
UploadMediaDialog,
|
||||||
} from "@shared";
|
} from "@shared";
|
||||||
|
|
||||||
export const RouteCreatePage = observer(() => {
|
export const RouteCreatePage = observer(() => {
|
||||||
@@ -37,8 +38,9 @@ export const RouteCreatePage = observer(() => {
|
|||||||
const [govRouteNumber, setGovRouteNumber] = useState("");
|
const [govRouteNumber, setGovRouteNumber] = useState("");
|
||||||
const [governorAppeal, setGovernorAppeal] = useState<string>("");
|
const [governorAppeal, setGovernorAppeal] = useState<string>("");
|
||||||
const [direction, setDirection] = useState("backward");
|
const [direction, setDirection] = useState("backward");
|
||||||
const [scaleMin, setScaleMin] = useState("");
|
const [scaleMin, setScaleMin] = useState("10");
|
||||||
const [scaleMax, setScaleMax] = useState("");
|
const [scaleMax, setScaleMax] = useState("100");
|
||||||
|
const [routeName, setRouteName] = useState("");
|
||||||
const [turn, setTurn] = useState("");
|
const [turn, setTurn] = useState("");
|
||||||
const [centerLat, setCenterLat] = useState("");
|
const [centerLat, setCenterLat] = useState("");
|
||||||
const [centerLng, setCenterLng] = useState("");
|
const [centerLng, setCenterLng] = useState("");
|
||||||
@@ -48,6 +50,8 @@ export const RouteCreatePage = observer(() => {
|
|||||||
useState(false);
|
useState(false);
|
||||||
const [isSelectVideoDialogOpen, setIsSelectVideoDialogOpen] = useState(false);
|
const [isSelectVideoDialogOpen, setIsSelectVideoDialogOpen] = useState(false);
|
||||||
const [isVideoPreviewOpen, setIsVideoPreviewOpen] = useState(false);
|
const [isVideoPreviewOpen, setIsVideoPreviewOpen] = useState(false);
|
||||||
|
const [isUploadVideoDialogOpen, setIsUploadVideoDialogOpen] = useState(false);
|
||||||
|
const [fileToUpload, setFileToUpload] = useState<File | null>(null);
|
||||||
const { language } = languageStore;
|
const { language } = languageStore;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -55,7 +59,6 @@ export const RouteCreatePage = observer(() => {
|
|||||||
articlesStore.getArticleList();
|
articlesStore.getArticleList();
|
||||||
}, [language]);
|
}, [language]);
|
||||||
|
|
||||||
// Фильтруем перевозчиков только из выбранного города
|
|
||||||
const filteredCarriers = useMemo(() => {
|
const filteredCarriers = useMemo(() => {
|
||||||
const carriers =
|
const carriers =
|
||||||
carrierStore.carriers[language as keyof typeof carrierStore.carriers]
|
carrierStore.carriers[language as keyof typeof carrierStore.carriers]
|
||||||
@@ -110,6 +113,7 @@ export const RouteCreatePage = observer(() => {
|
|||||||
const handleArticleSelect = (articleId: number) => {
|
const handleArticleSelect = (articleId: number) => {
|
||||||
setGovernorAppeal(articleId.toString());
|
setGovernorAppeal(articleId.toString());
|
||||||
setIsSelectArticleDialogOpen(false);
|
setIsSelectArticleDialogOpen(false);
|
||||||
|
articlesStore.getArticleList();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleVideoSelect = (media: {
|
const handleVideoSelect = (media: {
|
||||||
@@ -122,6 +126,26 @@ export const RouteCreatePage = observer(() => {
|
|||||||
setIsSelectVideoDialogOpen(false);
|
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 = () => {
|
const handleVideoPreviewClick = () => {
|
||||||
setIsVideoPreviewOpen(true);
|
setIsVideoPreviewOpen(true);
|
||||||
};
|
};
|
||||||
@@ -129,23 +153,72 @@ export const RouteCreatePage = observer(() => {
|
|||||||
const handleCreateRoute = async () => {
|
const handleCreateRoute = async () => {
|
||||||
try {
|
try {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
// Преобразуем значения в нужные типы
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
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 carrier_id = Number(carrier);
|
||||||
const governor_appeal = Number(governorAppeal);
|
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 rotate = turn ? Number(turn) : undefined;
|
||||||
const center_latitude = centerLat ? Number(centerLat) : undefined;
|
const center_latitude = centerLat ? Number(centerLat) : undefined;
|
||||||
const center_longitude = centerLng ? Number(centerLng) : undefined;
|
const center_longitude = centerLng ? Number(centerLng) : undefined;
|
||||||
const route_direction = direction === "forward";
|
const route_direction = direction === "forward";
|
||||||
|
|
||||||
const validationResult = validateCoordinates(routeCoords);
|
|
||||||
if (validationResult !== true) {
|
|
||||||
toast.error(validationResult);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Координаты маршрута как массив массивов чисел
|
|
||||||
const path = routeCoords
|
const path = routeCoords
|
||||||
.trim()
|
.trim()
|
||||||
.split("\n")
|
.split("\n")
|
||||||
@@ -157,7 +230,6 @@ export const RouteCreatePage = observer(() => {
|
|||||||
return [lat, lon];
|
return [lat, lon];
|
||||||
});
|
});
|
||||||
|
|
||||||
// Собираем объект маршрута
|
|
||||||
const newRoute: Partial<Route> = {
|
const newRoute: Partial<Route> = {
|
||||||
carrier:
|
carrier:
|
||||||
carrierStore.carriers[
|
carrierStore.carriers[
|
||||||
@@ -167,9 +239,10 @@ export const RouteCreatePage = observer(() => {
|
|||||||
route_number: routeNumber,
|
route_number: routeNumber,
|
||||||
route_sys_number: govRouteNumber,
|
route_sys_number: govRouteNumber,
|
||||||
governor_appeal,
|
governor_appeal,
|
||||||
|
route_name: routeName,
|
||||||
route_direction,
|
route_direction,
|
||||||
scale_min,
|
scale_min: scale_min !== null ? scale_min : 0,
|
||||||
scale_max,
|
scale_max: scale_max !== null ? scale_max : 0,
|
||||||
rotate,
|
rotate,
|
||||||
center_latitude,
|
center_latitude,
|
||||||
center_longitude,
|
center_longitude,
|
||||||
@@ -189,7 +262,6 @@ export const RouteCreatePage = observer(() => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Получаем название выбранной статьи для отображения
|
|
||||||
const selectedArticle = articlesStore.articleList.ru.data.find(
|
const selectedArticle = articlesStore.articleList.ru.data.find(
|
||||||
(article) => article.id === Number(governorAppeal)
|
(article) => article.id === Number(governorAppeal)
|
||||||
);
|
);
|
||||||
@@ -208,6 +280,13 @@ export const RouteCreatePage = observer(() => {
|
|||||||
|
|
||||||
<div className="flex flex-col gap-10 w-full items-end">
|
<div className="flex flex-col gap-10 w-full items-end">
|
||||||
<Box className="flex flex-col gap-6 w-full">
|
<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>
|
<FormControl fullWidth required>
|
||||||
<InputLabel>Выберите перевозчика</InputLabel>
|
<InputLabel>Выберите перевозчика</InputLabel>
|
||||||
<Select
|
<Select
|
||||||
@@ -247,7 +326,6 @@ export const RouteCreatePage = observer(() => {
|
|||||||
const lines = routeCoords.split("\n");
|
const lines = routeCoords.split("\n");
|
||||||
const lastLine = lines[lines.length - 1];
|
const lastLine = lines[lines.length - 1];
|
||||||
|
|
||||||
// Если мы на последней строке и она не пустая
|
|
||||||
if (lastLine && lastLine.trim()) {
|
if (lastLine && lastLine.trim()) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const newValue = routeCoords + "\n";
|
const newValue = routeCoords + "\n";
|
||||||
@@ -279,6 +357,7 @@ export const RouteCreatePage = observer(() => {
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
className="w-full"
|
className="w-full"
|
||||||
label="Номер маршрута в Говорящем Городе"
|
label="Номер маршрута в Говорящем Городе"
|
||||||
@@ -287,99 +366,42 @@ export const RouteCreatePage = observer(() => {
|
|||||||
onChange={(e) => setGovRouteNumber(e.target.value)}
|
onChange={(e) => setGovRouteNumber(e.target.value)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Заменяем Select на кнопку для выбора статьи */}
|
<Typography variant="subtitle1" sx={{ fontWeight: 600 }}>
|
||||||
<Box className="flex flex-col gap-2">
|
Обращение к пассажирам
|
||||||
<label className="text-sm font-medium text-gray-700">
|
</Typography>
|
||||||
Обращение к пассажирам
|
<Box className="flex gap-2">
|
||||||
</label>
|
<TextField
|
||||||
<Box className="flex gap-2">
|
className="flex-1"
|
||||||
<TextField
|
value={selectedArticle?.heading || "Статья не выбрана"}
|
||||||
className="flex-1"
|
placeholder="Выберите статью"
|
||||||
value={selectedArticle?.heading || "Статья не выбрана"}
|
disabled
|
||||||
placeholder="Выберите статью"
|
fullWidth
|
||||||
disabled
|
sx={{
|
||||||
sx={{
|
"& .MuiInputBase-input": {
|
||||||
"& .MuiInputBase-input": {
|
color: selectedArticle ? "inherit" : "#999",
|
||||||
color: selectedArticle ? "inherit" : "#999",
|
},
|
||||||
},
|
}}
|
||||||
}}
|
/>
|
||||||
/>
|
<Button
|
||||||
<Button
|
variant="outlined"
|
||||||
variant="outlined"
|
onClick={() => setIsSelectArticleDialogOpen(true)}
|
||||||
onClick={() => setIsSelectArticleDialogOpen(true)}
|
startIcon={<Plus size={16} />}
|
||||||
startIcon={<Plus size={16} />}
|
sx={{ minWidth: "auto", px: 2 }}
|
||||||
sx={{ minWidth: "auto", px: 2 }}
|
>
|
||||||
>
|
Выбрать
|
||||||
Выбрать
|
</Button>
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* Селектор видеозаставки */}
|
<VideoPreviewCard
|
||||||
<Box className="flex flex-col gap-2">
|
title="Видеозаставка"
|
||||||
<label className="text-sm font-medium text-gray-700">
|
videoId={videoPreview}
|
||||||
Видеозаставка
|
onVideoClick={handleVideoPreviewClick}
|
||||||
</label>
|
onDeleteVideoClick={() => {
|
||||||
<Box className="flex gap-2">
|
setVideoPreview("");
|
||||||
<Box
|
}}
|
||||||
className="flex-1"
|
onSelectVideoClick={handleVideoFileSelect}
|
||||||
onClick={handleVideoPreviewClick}
|
className="w-full"
|
||||||
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>
|
|
||||||
|
|
||||||
<FormControl fullWidth required>
|
<FormControl fullWidth required>
|
||||||
<InputLabel>Прямой/обратный маршрут</InputLabel>
|
<InputLabel>Прямой/обратный маршрут</InputLabel>
|
||||||
@@ -395,15 +417,41 @@ export const RouteCreatePage = observer(() => {
|
|||||||
<TextField
|
<TextField
|
||||||
className="w-full"
|
className="w-full"
|
||||||
label="Масштаб (мин)"
|
label="Масштаб (мин)"
|
||||||
|
type="number"
|
||||||
value={scaleMin}
|
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
|
<TextField
|
||||||
className="w-full"
|
className="w-full"
|
||||||
label="Масштаб (макс)"
|
label="Масштаб (макс)"
|
||||||
|
type="number"
|
||||||
value={scaleMax}
|
value={scaleMax}
|
||||||
onChange={(e) => setScaleMax(e.target.value)}
|
required
|
||||||
|
onChange={(e) => {
|
||||||
|
const value = e.target.value;
|
||||||
|
setScaleMax(value);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
className="w-full"
|
className="w-full"
|
||||||
label="Поворот"
|
label="Поворот"
|
||||||
@@ -440,23 +488,17 @@ export const RouteCreatePage = observer(() => {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<ArticleSelectOrCreateDialog
|
||||||
{/* Модальное окно выбора статьи */}
|
|
||||||
<SelectArticleModal
|
|
||||||
open={isSelectArticleDialogOpen}
|
open={isSelectArticleDialogOpen}
|
||||||
onClose={() => setIsSelectArticleDialogOpen(false)}
|
onClose={() => setIsSelectArticleDialogOpen(false)}
|
||||||
onSelectArticle={handleArticleSelect}
|
onSelectArticle={handleArticleSelect}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Модальное окно выбора видео */}
|
|
||||||
<SelectMediaDialog
|
<SelectMediaDialog
|
||||||
open={isSelectVideoDialogOpen}
|
open={isSelectVideoDialogOpen}
|
||||||
onClose={() => setIsSelectVideoDialogOpen(false)}
|
onClose={() => setIsSelectVideoDialogOpen(false)}
|
||||||
onSelectMedia={handleVideoSelect}
|
onSelectMedia={handleVideoSelect}
|
||||||
mediaType={2}
|
mediaType={2}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Модальное окно предпросмотра видео */}
|
|
||||||
{videoPreview && videoPreview !== "" && (
|
{videoPreview && videoPreview !== "" && (
|
||||||
<Dialog
|
<Dialog
|
||||||
open={isVideoPreviewOpen}
|
open={isVideoPreviewOpen}
|
||||||
@@ -483,6 +525,18 @@ export const RouteCreatePage = observer(() => {
|
|||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)}
|
)}
|
||||||
|
<UploadMediaDialog
|
||||||
|
open={isUploadVideoDialogOpen}
|
||||||
|
onClose={() => {
|
||||||
|
setIsUploadVideoDialogOpen(false);
|
||||||
|
setFileToUpload(null);
|
||||||
|
}}
|
||||||
|
hardcodeType="video_preview"
|
||||||
|
contextObjectName={routeName || "Маршрут"}
|
||||||
|
contextType="sight"
|
||||||
|
initialFile={fileToUpload || undefined}
|
||||||
|
afterUpload={handleVideoUpload}
|
||||||
|
/>
|
||||||
</Paper>
|
</Paper>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
DialogContent,
|
DialogContent,
|
||||||
DialogActions,
|
DialogActions,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import { MediaViewer } from "@widgets";
|
import { MediaViewer, VideoPreviewCard } from "@widgets";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { ArrowLeft, Copy, Save, Plus } from "lucide-react";
|
import { ArrowLeft, Copy, Save, Plus } from "lucide-react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
@@ -24,8 +24,9 @@ import { articlesStore } from "../../../shared/store/ArticlesStore";
|
|||||||
import {
|
import {
|
||||||
routeStore,
|
routeStore,
|
||||||
languageStore,
|
languageStore,
|
||||||
SelectArticleModal,
|
ArticleSelectOrCreateDialog,
|
||||||
SelectMediaDialog,
|
SelectMediaDialog,
|
||||||
|
UploadMediaDialog,
|
||||||
} from "@shared";
|
} from "@shared";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
import { stationsStore } from "@shared";
|
import { stationsStore } from "@shared";
|
||||||
@@ -40,12 +41,13 @@ export const RouteEditPage = observer(() => {
|
|||||||
useState(false);
|
useState(false);
|
||||||
const [isSelectVideoDialogOpen, setIsSelectVideoDialogOpen] = useState(false);
|
const [isSelectVideoDialogOpen, setIsSelectVideoDialogOpen] = useState(false);
|
||||||
const [isVideoPreviewOpen, setIsVideoPreviewOpen] = useState(false);
|
const [isVideoPreviewOpen, setIsVideoPreviewOpen] = useState(false);
|
||||||
|
const [isUploadVideoDialogOpen, setIsUploadVideoDialogOpen] = useState(false);
|
||||||
|
const [fileToUpload, setFileToUpload] = useState<File | null>(null);
|
||||||
const { language } = languageStore;
|
const { language } = languageStore;
|
||||||
const [coordinates, setCoordinates] = useState<string>("");
|
const [coordinates, setCoordinates] = useState<string>("");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
// Устанавливаем русский язык при загрузке страницы
|
|
||||||
const response = await routeStore.getRoute(Number(id));
|
const response = await routeStore.getRoute(Number(id));
|
||||||
routeStore.setEditRouteData(response);
|
routeStore.setEditRouteData(response);
|
||||||
languageStore.setLanguage("ru");
|
languageStore.setLanguage("ru");
|
||||||
@@ -72,10 +74,67 @@ export const RouteEditPage = observer(() => {
|
|||||||
}, [editRouteData.path]);
|
}, [editRouteData.path]);
|
||||||
|
|
||||||
const handleSave = async () => {
|
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);
|
setIsLoading(true);
|
||||||
await routeStore.editRoute(Number(id));
|
try {
|
||||||
toast.success("Маршрут успешно сохранен");
|
await routeStore.editRoute(Number(id));
|
||||||
setIsLoading(false);
|
toast.success("Маршрут успешно сохранен");
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
toast.error("Произошла ошибка при сохранении маршрута");
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const validateCoordinates = (value: string) => {
|
const validateCoordinates = (value: string) => {
|
||||||
@@ -125,6 +184,8 @@ export const RouteEditPage = observer(() => {
|
|||||||
governor_appeal: articleId,
|
governor_appeal: articleId,
|
||||||
});
|
});
|
||||||
setIsSelectArticleDialogOpen(false);
|
setIsSelectArticleDialogOpen(false);
|
||||||
|
// Обновляем список статей после создания новой
|
||||||
|
articlesStore.getArticleList();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleVideoSelect = (media: {
|
const handleVideoSelect = (media: {
|
||||||
@@ -139,6 +200,28 @@ export const RouteEditPage = observer(() => {
|
|||||||
setIsSelectVideoDialogOpen(false);
|
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 = () => {
|
const handleVideoPreviewClick = () => {
|
||||||
if (editRouteData.video_preview && editRouteData.video_preview !== "") {
|
if (editRouteData.video_preview && editRouteData.video_preview !== "") {
|
||||||
setIsVideoPreviewOpen(true);
|
setIsVideoPreviewOpen(true);
|
||||||
@@ -164,6 +247,17 @@ export const RouteEditPage = observer(() => {
|
|||||||
|
|
||||||
<div className="flex flex-col gap-10 w-full items-end">
|
<div className="flex flex-col gap-10 w-full items-end">
|
||||||
<Box className="flex flex-col gap-6 w-full">
|
<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>
|
<FormControl fullWidth required>
|
||||||
<InputLabel>Выберите перевозчика</InputLabel>
|
<InputLabel>Выберите перевозчика</InputLabel>
|
||||||
<Select
|
<Select
|
||||||
@@ -235,7 +329,6 @@ export const RouteEditPage = observer(() => {
|
|||||||
const lines = coordinates.split("\n");
|
const lines = coordinates.split("\n");
|
||||||
const lastLine = lines[lines.length - 1];
|
const lastLine = lines[lines.length - 1];
|
||||||
|
|
||||||
// Если мы на последней строке и она не пустая
|
|
||||||
if (lastLine && lastLine.trim()) {
|
if (lastLine && lastLine.trim()) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const newValue = coordinates + "\n";
|
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>
|
<FormControl fullWidth required>
|
||||||
<InputLabel>Прямой/обратный маршрут</InputLabel>
|
<InputLabel>Прямой/обратный маршрут</InputLabel>
|
||||||
<Select
|
<Select
|
||||||
@@ -401,17 +390,33 @@ export const RouteEditPage = observer(() => {
|
|||||||
<TextField
|
<TextField
|
||||||
className="w-full"
|
className="w-full"
|
||||||
label="Масштаб (мин)"
|
label="Масштаб (мин)"
|
||||||
|
type="number"
|
||||||
value={editRouteData.scale_min ?? ""}
|
value={editRouteData.scale_min ?? ""}
|
||||||
onChange={(e) =>
|
onChange={(e) => {
|
||||||
|
const value =
|
||||||
|
e.target.value === "" ? null : parseFloat(e.target.value);
|
||||||
routeStore.setEditRouteData({
|
routeStore.setEditRouteData({
|
||||||
scale_min:
|
scale_min: value,
|
||||||
e.target.value === "" ? null : parseFloat(e.target.value),
|
});
|
||||||
})
|
// Если максимальный масштаб стал меньше минимального, обновляем его
|
||||||
}
|
if (
|
||||||
|
value !== null &&
|
||||||
|
editRouteData.scale_max !== null &&
|
||||||
|
editRouteData.scale_max !== undefined &&
|
||||||
|
value > editRouteData.scale_max
|
||||||
|
) {
|
||||||
|
routeStore.setEditRouteData({
|
||||||
|
scale_max: value,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
className="w-full"
|
className="w-full"
|
||||||
|
required
|
||||||
label="Масштаб (макс)"
|
label="Масштаб (макс)"
|
||||||
|
type="number"
|
||||||
value={editRouteData.scale_max ?? ""}
|
value={editRouteData.scale_max ?? ""}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
routeStore.setEditRouteData({
|
routeStore.setEditRouteData({
|
||||||
@@ -419,6 +424,22 @@ export const RouteEditPage = observer(() => {
|
|||||||
e.target.value === "" ? null : parseFloat(e.target.value),
|
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
|
<TextField
|
||||||
className="w-full"
|
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>
|
</Box>
|
||||||
|
|
||||||
<LinkedItems
|
<LinkedItems
|
||||||
@@ -493,23 +551,17 @@ export const RouteEditPage = observer(() => {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<ArticleSelectOrCreateDialog
|
||||||
{/* Модальное окно выбора статьи */}
|
|
||||||
<SelectArticleModal
|
|
||||||
open={isSelectArticleDialogOpen}
|
open={isSelectArticleDialogOpen}
|
||||||
onClose={() => setIsSelectArticleDialogOpen(false)}
|
onClose={() => setIsSelectArticleDialogOpen(false)}
|
||||||
onSelectArticle={handleArticleSelect}
|
onSelectArticle={handleArticleSelect}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Модальное окно выбора видео */}
|
|
||||||
<SelectMediaDialog
|
<SelectMediaDialog
|
||||||
open={isSelectVideoDialogOpen}
|
open={isSelectVideoDialogOpen}
|
||||||
onClose={() => setIsSelectVideoDialogOpen(false)}
|
onClose={() => setIsSelectVideoDialogOpen(false)}
|
||||||
onSelectMedia={handleVideoSelect}
|
onSelectMedia={handleVideoSelect}
|
||||||
mediaType={2}
|
mediaType={2}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Модальное окно предпросмотра видео */}
|
|
||||||
<Dialog
|
<Dialog
|
||||||
open={isVideoPreviewOpen}
|
open={isVideoPreviewOpen}
|
||||||
onClose={() => setIsVideoPreviewOpen(false)}
|
onClose={() => setIsVideoPreviewOpen(false)}
|
||||||
@@ -519,19 +571,33 @@ export const RouteEditPage = observer(() => {
|
|||||||
<DialogTitle>Предпросмотр видео</DialogTitle>
|
<DialogTitle>Предпросмотр видео</DialogTitle>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<Box className="flex justify-center items-center p-4">
|
<Box className="flex justify-center items-center p-4">
|
||||||
<MediaViewer
|
{editRouteData.video_preview && (
|
||||||
media={{
|
<MediaViewer
|
||||||
id: editRouteData.video_preview,
|
media={{
|
||||||
media_type: 2,
|
id: editRouteData.video_preview,
|
||||||
filename: "video_preview",
|
media_type: 2,
|
||||||
}}
|
filename: "video_preview",
|
||||||
/>
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button onClick={() => setIsVideoPreviewOpen(false)}>Закрыть</Button>
|
<Button onClick={() => setIsVideoPreviewOpen(false)}>Закрыть</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
<UploadMediaDialog
|
||||||
|
open={isUploadVideoDialogOpen}
|
||||||
|
onClose={() => {
|
||||||
|
setIsUploadVideoDialogOpen(false);
|
||||||
|
setFileToUpload(null);
|
||||||
|
}}
|
||||||
|
hardcodeType="video_preview"
|
||||||
|
contextObjectName={editRouteData.route_name || "Маршрут"}
|
||||||
|
contextType="sight"
|
||||||
|
initialFile={fileToUpload || undefined}
|
||||||
|
afterUpload={handleVideoUpload}
|
||||||
|
/>
|
||||||
</Paper>
|
</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",
|
field: "route_number",
|
||||||
headerName: "Номер маршрута",
|
headerName: "Номер маршрута",
|
||||||
@@ -100,9 +116,7 @@ export const RouteListPage = observer(() => {
|
|||||||
<button onClick={() => navigate(`/route-preview/${params.row.id}`)}>
|
<button onClick={() => navigate(`/route-preview/${params.row.id}`)}>
|
||||||
<Map size={20} className="text-purple-500" />
|
<Map size={20} className="text-purple-500" />
|
||||||
</button>
|
</button>
|
||||||
{/* <button onClick={() => navigate(`/route/${params.row.id}`)}>
|
|
||||||
<Eye size={20} className="text-green-500" />
|
|
||||||
</button> */}
|
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setIsDeleteModalOpen(true);
|
setIsDeleteModalOpen(true);
|
||||||
@@ -122,6 +136,7 @@ export const RouteListPage = observer(() => {
|
|||||||
carrier_id: route.carrier_id,
|
carrier_id: route.carrier_id,
|
||||||
route_number: route.route_number,
|
route_number: route.route_number,
|
||||||
route_direction: route.route_direction ? "Прямой" : "Обратный",
|
route_direction: route.route_direction ? "Прямой" : "Обратный",
|
||||||
|
route_name: route.route_name,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -47,10 +47,8 @@ export function InfiniteCanvas({
|
|||||||
const [startPosition, setStartPosition] = useState({ x: 0, y: 0 });
|
const [startPosition, setStartPosition] = useState({ x: 0, y: 0 });
|
||||||
const [isPointerDown, setIsPointerDown] = useState(false);
|
const [isPointerDown, setIsPointerDown] = useState(false);
|
||||||
|
|
||||||
// Флаг для предотвращения конфликта между пользовательским вводом и данными маршрута
|
|
||||||
const [isUserInteracting, setIsUserInteracting] = useState(false);
|
const [isUserInteracting, setIsUserInteracting] = useState(false);
|
||||||
|
|
||||||
// Реф для отслеживания последнего значения originalRouteData?.rotate
|
|
||||||
const lastOriginalRotation = useRef<number | undefined>(undefined);
|
const lastOriginalRotation = useRef<number | undefined>(undefined);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -68,7 +66,7 @@ export function InfiniteCanvas({
|
|||||||
const handlePointerDown = (e: FederatedMouseEvent) => {
|
const handlePointerDown = (e: FederatedMouseEvent) => {
|
||||||
setIsPointerDown(true);
|
setIsPointerDown(true);
|
||||||
setIsDragging(false);
|
setIsDragging(false);
|
||||||
setIsUserInteracting(true); // Устанавливаем флаг взаимодействия пользователя
|
setIsUserInteracting(true);
|
||||||
setStartPosition({
|
setStartPosition({
|
||||||
x: position.x,
|
x: position.x,
|
||||||
y: position.y,
|
y: position.y,
|
||||||
@@ -81,13 +79,9 @@ export function InfiniteCanvas({
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
};
|
};
|
||||||
|
|
||||||
// Устанавливаем rotation только при изменении originalRouteData и отсутствии взаимодействия пользователя
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const newRotation = originalRouteData?.rotate ?? 0;
|
const newRotation = originalRouteData?.rotate ?? 0;
|
||||||
|
|
||||||
// Обновляем rotation только если:
|
|
||||||
// 1. Пользователь не взаимодействует с канвасом
|
|
||||||
// 2. Значение действительно изменилось
|
|
||||||
if (!isUserInteracting && lastOriginalRotation.current !== newRotation) {
|
if (!isUserInteracting && lastOriginalRotation.current !== newRotation) {
|
||||||
setRotation((newRotation * Math.PI) / 180);
|
setRotation((newRotation * Math.PI) / 180);
|
||||||
lastOriginalRotation.current = newRotation;
|
lastOriginalRotation.current = newRotation;
|
||||||
@@ -97,7 +91,6 @@ export function InfiniteCanvas({
|
|||||||
const handlePointerMove = (e: FederatedMouseEvent) => {
|
const handlePointerMove = (e: FederatedMouseEvent) => {
|
||||||
if (!isPointerDown) return;
|
if (!isPointerDown) return;
|
||||||
|
|
||||||
// Проверяем, началось ли перетаскивание
|
|
||||||
if (!isDragging) {
|
if (!isDragging) {
|
||||||
const dx = e.globalX - startMousePosition.x;
|
const dx = e.globalX - startMousePosition.x;
|
||||||
const dy = e.globalY - startMousePosition.y;
|
const dy = e.globalY - startMousePosition.y;
|
||||||
@@ -119,10 +112,8 @@ export function InfiniteCanvas({
|
|||||||
e.globalX - center.x
|
e.globalX - center.x
|
||||||
);
|
);
|
||||||
|
|
||||||
// Calculate rotation difference in radians
|
|
||||||
const rotationDiff = currentAngle - startAngle;
|
const rotationDiff = currentAngle - startAngle;
|
||||||
|
|
||||||
// Update rotation
|
|
||||||
setRotation(startRotation + rotationDiff);
|
setRotation(startRotation + rotationDiff);
|
||||||
|
|
||||||
const cosDelta = Math.cos(rotationDiff);
|
const cosDelta = Math.cos(rotationDiff);
|
||||||
@@ -149,15 +140,13 @@ export function InfiniteCanvas({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handlePointerUp = (e: FederatedMouseEvent) => {
|
const handlePointerUp = (e: FederatedMouseEvent) => {
|
||||||
// Если не было перетаскивания, то это простой клик - закрываем виджет
|
|
||||||
if (!isDragging) {
|
if (!isDragging) {
|
||||||
setSelectedSight(undefined);
|
setSelectedSight(undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsPointerDown(false);
|
setIsPointerDown(false);
|
||||||
setIsDragging(false);
|
setIsDragging(false);
|
||||||
// Сбрасываем флаг взаимодействия через небольшую задержку
|
|
||||||
// чтобы избежать немедленного срабатывания useEffect
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setIsUserInteracting(false);
|
setIsUserInteracting(false);
|
||||||
}, 100);
|
}, 100);
|
||||||
@@ -166,29 +155,25 @@ export function InfiniteCanvas({
|
|||||||
|
|
||||||
const handleWheel = (e: FederatedWheelEvent) => {
|
const handleWheel = (e: FederatedWheelEvent) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
setIsUserInteracting(true); // Устанавливаем флаг при зуме
|
setIsUserInteracting(true);
|
||||||
|
|
||||||
// Get mouse position relative to canvas
|
|
||||||
const mouseX = e.globalX - position.x;
|
const mouseX = e.globalX - position.x;
|
||||||
const mouseY = e.globalY - position.y;
|
const mouseY = e.globalY - position.y;
|
||||||
|
|
||||||
// Calculate new scale
|
|
||||||
const scaleMin = (routeData?.scale_min ?? 10) / SCALE_FACTOR;
|
const scaleMin = (routeData?.scale_min ?? 10) / SCALE_FACTOR;
|
||||||
const scaleMax = (routeData?.scale_max ?? 20) / SCALE_FACTOR;
|
const scaleMax = (routeData?.scale_max ?? 20) / SCALE_FACTOR;
|
||||||
|
|
||||||
const zoomFactor = e.deltaY > 0 ? 0.9 : 1.1; // Zoom out/in
|
const zoomFactor = e.deltaY > 0 ? 0.9 : 1.1;
|
||||||
const newScale = Math.max(scaleMin, Math.min(scaleMax, scale * zoomFactor));
|
const newScale = Math.max(scaleMin, Math.min(scaleMax, scale * zoomFactor));
|
||||||
const actualZoomFactor = newScale / scale;
|
const actualZoomFactor = newScale / scale;
|
||||||
|
|
||||||
if (scale === newScale) {
|
if (scale === newScale) {
|
||||||
// Сбрасываем флаг, если зум не изменился
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setIsUserInteracting(false);
|
setIsUserInteracting(false);
|
||||||
}, 100);
|
}, 100);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update position to zoom towards mouse cursor
|
|
||||||
setPosition({
|
setPosition({
|
||||||
x: position.x + mouseX * (1 - actualZoomFactor),
|
x: position.x + mouseX * (1 - actualZoomFactor),
|
||||||
y: position.y + mouseY * (1 - actualZoomFactor),
|
y: position.y + mouseY * (1 - actualZoomFactor),
|
||||||
@@ -196,7 +181,6 @@ export function InfiniteCanvas({
|
|||||||
|
|
||||||
setScale(newScale);
|
setScale(newScale);
|
||||||
|
|
||||||
// Сбрасываем флаг взаимодействия через задержку
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setIsUserInteracting(false);
|
setIsUserInteracting(false);
|
||||||
}, 100);
|
}, 100);
|
||||||
|
|||||||
@@ -141,7 +141,6 @@ export const MapDataProvider = observer(
|
|||||||
}, [routeId]);
|
}, [routeId]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// combine changes with original data
|
|
||||||
if (originalRouteData)
|
if (originalRouteData)
|
||||||
setRouteData({ ...originalRouteData, ...routeChanges });
|
setRouteData({ ...originalRouteData, ...routeChanges });
|
||||||
if (originalSightData) setSightData(originalSightData);
|
if (originalSightData) setSightData(originalSightData);
|
||||||
|
|||||||
@@ -37,11 +37,9 @@ export function RightSidebar() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (originalRouteData) {
|
if (originalRouteData) {
|
||||||
// Проверяем и сбрасываем минимальный масштаб если нужно
|
|
||||||
const originalMinScale = originalRouteData.scale_min ?? 1;
|
const originalMinScale = originalRouteData.scale_min ?? 1;
|
||||||
const resetMinScale = originalMinScale < 1 ? 1 : originalMinScale;
|
const resetMinScale = originalMinScale < 1 ? 1 : originalMinScale;
|
||||||
|
|
||||||
// Проверяем и сбрасываем максимальный масштаб если нужно
|
|
||||||
const originalMaxScale = originalRouteData.scale_max ?? 5;
|
const originalMaxScale = originalRouteData.scale_max ?? 5;
|
||||||
const resetMaxScale = originalMaxScale < 3 ? 3 : originalMaxScale;
|
const resetMaxScale = originalMaxScale < 3 ? 3 : originalMaxScale;
|
||||||
|
|
||||||
@@ -118,7 +116,7 @@ export function RightSidebar() {
|
|||||||
borderRadius={2}
|
borderRadius={2}
|
||||||
>
|
>
|
||||||
<Typography variant="h6" sx={{ mb: 2, color: "#fff" }} textAlign="center">
|
<Typography variant="h6" sx={{ mb: 2, color: "#fff" }} textAlign="center">
|
||||||
Детали о достопримечательностях
|
Настройка маршрута
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<Stack spacing={2} direction="row" alignItems="center">
|
<Stack spacing={2} direction="row" alignItems="center">
|
||||||
@@ -130,7 +128,6 @@ export function RightSidebar() {
|
|||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
let newMinScale = Number(e.target.value);
|
let newMinScale = Number(e.target.value);
|
||||||
|
|
||||||
// Сбрасываем к 1 если меньше
|
|
||||||
if (newMinScale < 1) {
|
if (newMinScale < 1) {
|
||||||
newMinScale = 1;
|
newMinScale = 1;
|
||||||
}
|
}
|
||||||
@@ -139,10 +136,10 @@ export function RightSidebar() {
|
|||||||
|
|
||||||
if (maxScale - newMinScale < 2) {
|
if (maxScale - newMinScale < 2) {
|
||||||
let newMaxScale = newMinScale + 2;
|
let newMaxScale = newMinScale + 2;
|
||||||
// Сбрасываем максимальный к 3 если меньше минимального
|
|
||||||
if (newMaxScale < 3) {
|
if (newMaxScale < 3) {
|
||||||
newMaxScale = 3;
|
newMaxScale = 3;
|
||||||
setMinScale(1); // Сбрасываем минимальный к 1
|
setMinScale(1);
|
||||||
}
|
}
|
||||||
setMaxScale(newMaxScale);
|
setMaxScale(newMaxScale);
|
||||||
}
|
}
|
||||||
@@ -175,7 +172,6 @@ export function RightSidebar() {
|
|||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
let newMaxScale = Number(e.target.value);
|
let newMaxScale = Number(e.target.value);
|
||||||
|
|
||||||
// Сбрасываем к 3 если меньше минимального
|
|
||||||
if (newMaxScale < 3) {
|
if (newMaxScale < 3) {
|
||||||
newMaxScale = 3;
|
newMaxScale = 3;
|
||||||
}
|
}
|
||||||
@@ -184,10 +180,10 @@ export function RightSidebar() {
|
|||||||
|
|
||||||
if (newMaxScale - minScale < 2) {
|
if (newMaxScale - minScale < 2) {
|
||||||
let newMinScale = newMaxScale - 2;
|
let newMinScale = newMaxScale - 2;
|
||||||
// Сбрасываем минимальный к 1 если меньше
|
|
||||||
if (newMinScale < 1) {
|
if (newMinScale < 1) {
|
||||||
newMinScale = 1;
|
newMinScale = 1;
|
||||||
setMaxScale(3); // Сбрасываем максимальный к минимальному значению
|
setMaxScale(3);
|
||||||
}
|
}
|
||||||
setMinScale(newMinScale);
|
setMinScale(newMinScale);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ export const Sight = ({ sight, id }: Readonly<SightProps>) => {
|
|||||||
|
|
||||||
const [texture, setTexture] = useState(Texture.EMPTY);
|
const [texture, setTexture] = useState(Texture.EMPTY);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
Assets.load("/SightIcon.png").then(setTexture);
|
Assets.load("/sight_icon.svg").then(setTexture);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {}, [id, sight.latitude, sight.longitude]);
|
useEffect(() => {}, [id, sight.latitude, sight.longitude]);
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { FederatedMouseEvent, Graphics } from "pixi.js";
|
|||||||
import { useCallback, useState, useEffect, useRef, FC, useMemo } from "react";
|
import { useCallback, useState, useEffect, useRef, FC, useMemo } from "react";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
|
|
||||||
// --- Заглушки для зависимостей (замените на ваши реальные импорты) ---
|
|
||||||
import {
|
import {
|
||||||
BACKGROUND_COLOR,
|
BACKGROUND_COLOR,
|
||||||
PATH_COLOR,
|
PATH_COLOR,
|
||||||
@@ -15,22 +14,16 @@ import { StationData } from "./types";
|
|||||||
import { useMapData } from "./MapDataContext";
|
import { useMapData } from "./MapDataContext";
|
||||||
import { coordinatesToLocal } from "./utils";
|
import { coordinatesToLocal } from "./utils";
|
||||||
import { languageStore } from "@shared";
|
import { languageStore } from "@shared";
|
||||||
// --- Конец заглушек ---
|
|
||||||
|
|
||||||
// --- Декларации для react-pixi ---
|
|
||||||
// (В реальном проекте типы должны быть предоставлены библиотекой @pixi/react-pixi)
|
|
||||||
declare const pixiContainer: any;
|
declare const pixiContainer: any;
|
||||||
declare const pixiGraphics: any;
|
declare const pixiGraphics: any;
|
||||||
declare const pixiText: any;
|
declare const pixiText: any;
|
||||||
|
|
||||||
// --- Типы ---
|
|
||||||
type HorizontalAlign = "left" | "center" | "right";
|
type HorizontalAlign = "left" | "center" | "right";
|
||||||
type VerticalAlign = "top" | "center" | "bottom";
|
type VerticalAlign = "top" | "center" | "bottom";
|
||||||
type TextAlign = HorizontalAlign | `${HorizontalAlign} ${VerticalAlign}`;
|
type TextAlign = HorizontalAlign | `${HorizontalAlign} ${VerticalAlign}`;
|
||||||
type LabelAlign = "left" | "center" | "right";
|
type LabelAlign = "left" | "center" | "right";
|
||||||
|
|
||||||
// --- Утилиты ---
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Преобразует текстовое позиционирование в anchor координаты.
|
* Преобразует текстовое позиционирование в anchor координаты.
|
||||||
*/
|
*/
|
||||||
@@ -39,8 +32,6 @@ type LabelAlign = "left" | "center" | "right";
|
|||||||
* Получает координату anchor.x из типа выравнивания.
|
* Получает координату anchor.x из типа выравнивания.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// --- Интерфейсы пропсов ---
|
|
||||||
|
|
||||||
interface StationProps {
|
interface StationProps {
|
||||||
station: StationData;
|
station: StationData;
|
||||||
ruLabel: string | null;
|
ruLabel: string | null;
|
||||||
@@ -83,10 +74,6 @@ const getAnchorFromOffset = (
|
|||||||
return { x: (1 - nx) / 2, y: (1 - ny) / 2 };
|
return { x: (1 - nx) / 2, y: (1 - ny) / 2 };
|
||||||
};
|
};
|
||||||
|
|
||||||
// =========================================================================
|
|
||||||
// Компонент: Панель управления выравниванием в стиле УрФУ
|
|
||||||
// =========================================================================
|
|
||||||
|
|
||||||
const LabelAlignmentControl: FC<LabelAlignmentControlProps> = ({
|
const LabelAlignmentControl: FC<LabelAlignmentControlProps> = ({
|
||||||
scale,
|
scale,
|
||||||
currentAlign,
|
currentAlign,
|
||||||
@@ -107,7 +94,6 @@ const LabelAlignmentControl: FC<LabelAlignmentControlProps> = ({
|
|||||||
(g: Graphics) => {
|
(g: Graphics) => {
|
||||||
g.clear();
|
g.clear();
|
||||||
|
|
||||||
// Основной фон с градиентом
|
|
||||||
g.roundRect(
|
g.roundRect(
|
||||||
-controlWidth / 2,
|
-controlWidth / 2,
|
||||||
0,
|
0,
|
||||||
@@ -115,9 +101,8 @@ const LabelAlignmentControl: FC<LabelAlignmentControlProps> = ({
|
|||||||
controlHeight,
|
controlHeight,
|
||||||
borderRadius
|
borderRadius
|
||||||
);
|
);
|
||||||
g.fill({ color: "#1a1a1a" }); // Темный фон как у УрФУ
|
g.fill({ color: "#1a1a1a" });
|
||||||
|
|
||||||
// Тонкая рамка
|
|
||||||
g.roundRect(
|
g.roundRect(
|
||||||
-controlWidth / 2,
|
-controlWidth / 2,
|
||||||
0,
|
0,
|
||||||
@@ -127,7 +112,6 @@ const LabelAlignmentControl: FC<LabelAlignmentControlProps> = ({
|
|||||||
);
|
);
|
||||||
g.stroke({ color: "#333333", width: strokeWidth });
|
g.stroke({ color: "#333333", width: strokeWidth });
|
||||||
|
|
||||||
// Разделители между кнопками
|
|
||||||
for (let i = 1; i < 3; i++) {
|
for (let i = 1; i < 3; i++) {
|
||||||
const x = -controlWidth / 2 + buttonWidth * i;
|
const x = -controlWidth / 2 + buttonWidth * i;
|
||||||
g.moveTo(x, strokeWidth);
|
g.moveTo(x, strokeWidth);
|
||||||
@@ -151,7 +135,7 @@ const LabelAlignmentControl: FC<LabelAlignmentControlProps> = ({
|
|||||||
controlHeight - strokeWidth * 2,
|
controlHeight - strokeWidth * 2,
|
||||||
borderRadius / 2
|
borderRadius / 2
|
||||||
);
|
);
|
||||||
g.fill({ color: "#0066cc", alpha: 0.8 }); // Синий акцент УрФУ
|
g.fill({ color: "#0066cc", alpha: 0.8 });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[controlWidth, controlHeight, buttonWidth, strokeWidth, borderRadius]
|
[controlWidth, controlHeight, buttonWidth, strokeWidth, borderRadius]
|
||||||
@@ -230,10 +214,6 @@ const LabelAlignmentControl: FC<LabelAlignmentControlProps> = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// =========================================================================
|
|
||||||
// Компонент: Метка Станции (с логикой)
|
|
||||||
// =========================================================================
|
|
||||||
|
|
||||||
const StationLabel = observer(
|
const StationLabel = observer(
|
||||||
({
|
({
|
||||||
station,
|
station,
|
||||||
@@ -274,48 +254,45 @@ const StationLabel = observer(
|
|||||||
hideTimer.current = null;
|
hideTimer.current = null;
|
||||||
}
|
}
|
||||||
setIsHovered(true);
|
setIsHovered(true);
|
||||||
onTextHover?.(true); // Call the callback to indicate text is hovered
|
onTextHover?.(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleControlPointerEnter = () => {
|
const handleControlPointerEnter = () => {
|
||||||
// Дополнительная обработка для панели управления
|
|
||||||
if (hideTimer.current) {
|
if (hideTimer.current) {
|
||||||
clearTimeout(hideTimer.current);
|
clearTimeout(hideTimer.current);
|
||||||
hideTimer.current = null;
|
hideTimer.current = null;
|
||||||
}
|
}
|
||||||
setIsControlHovered(true);
|
setIsControlHovered(true);
|
||||||
setIsHovered(true);
|
setIsHovered(true);
|
||||||
onTextHover?.(true); // Call the callback to indicate text/control is hovered
|
onTextHover?.(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleControlPointerLeave = () => {
|
const handleControlPointerLeave = () => {
|
||||||
setIsControlHovered(false);
|
setIsControlHovered(false);
|
||||||
// Если курсор не над основным контейнером, скрываем панель через некоторое время
|
|
||||||
if (!isHovered) {
|
if (!isHovered) {
|
||||||
hideTimer.current = setTimeout(() => {
|
hideTimer.current = setTimeout(() => {
|
||||||
setIsHovered(false);
|
setIsHovered(false);
|
||||||
onTextHover?.(false); // Call the callback to indicate neither text nor control is hovered
|
onTextHover?.(false);
|
||||||
}, 0);
|
}, 0);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePointerLeave = () => {
|
const handlePointerLeave = () => {
|
||||||
// Увеличиваем время до скрытия панели и добавляем проверку
|
|
||||||
hideTimer.current = setTimeout(() => {
|
hideTimer.current = setTimeout(() => {
|
||||||
setIsHovered(false);
|
setIsHovered(false);
|
||||||
// Если курсор не над панелью управления, скрываем и её
|
|
||||||
if (!isControlHovered) {
|
if (!isControlHovered) {
|
||||||
setIsControlHovered(false);
|
setIsControlHovered(false);
|
||||||
}
|
}
|
||||||
onTextHover?.(false); // Call the callback to indicate text is no longer hovered
|
onTextHover?.(false);
|
||||||
}, 100); // Увеличиваем время до скрытия панели
|
}, 100);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setPosition({ x: station.offset_x ?? 0, y: station.offset_y ?? 0 });
|
setPosition({ x: station.offset_x ?? 0, y: station.offset_y ?? 0 });
|
||||||
}, [station.offset_x, station.offset_y, station.id]);
|
}, [station.offset_x, station.offset_y, station.id]);
|
||||||
|
|
||||||
// Функция для конвертации числового align в строковый
|
|
||||||
const convertNumericAlign = (align: number): LabelAlign => {
|
const convertNumericAlign = (align: number): LabelAlign => {
|
||||||
switch (align) {
|
switch (align) {
|
||||||
case 0:
|
case 0:
|
||||||
@@ -329,7 +306,6 @@ const StationLabel = observer(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Функция для конвертации строкового align в числовой
|
|
||||||
const convertStringAlign = (align: LabelAlign): number => {
|
const convertStringAlign = (align: LabelAlign): number => {
|
||||||
switch (align) {
|
switch (align) {
|
||||||
case "left":
|
case "left":
|
||||||
@@ -353,7 +329,6 @@ const StationLabel = observer(
|
|||||||
const compensatedRuFontSize = (26 * 0.75) / scale;
|
const compensatedRuFontSize = (26 * 0.75) / scale;
|
||||||
const compensatedNameFontSize = (16 * 0.75) / scale;
|
const compensatedNameFontSize = (16 * 0.75) / scale;
|
||||||
|
|
||||||
// Измеряем ширину верхнего лейбла
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (ruLabelRef.current && ruLabel) {
|
if (ruLabelRef.current && ruLabel) {
|
||||||
setRuLabelWidth(ruLabelRef.current.width);
|
setRuLabelWidth(ruLabelRef.current.width);
|
||||||
@@ -386,7 +361,6 @@ const StationLabel = observer(
|
|||||||
y: dragStartPos.current.y + dy_screen,
|
y: dragStartPos.current.y + dy_screen,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Проверяем, изменилась ли позиция
|
|
||||||
if (
|
if (
|
||||||
Math.abs(newPosition.x - position.x) > 0.01 ||
|
Math.abs(newPosition.x - position.x) > 0.01 ||
|
||||||
Math.abs(newPosition.y - position.y) > 0.01
|
Math.abs(newPosition.y - position.y) > 0.01
|
||||||
@@ -406,7 +380,7 @@ const StationLabel = observer(
|
|||||||
const handleAlignChange = async (align: LabelAlign) => {
|
const handleAlignChange = async (align: LabelAlign) => {
|
||||||
setCurrentLabelAlign(align);
|
setCurrentLabelAlign(align);
|
||||||
onLabelAlignChange?.(align);
|
onLabelAlignChange?.(align);
|
||||||
// Сохраняем в стор
|
|
||||||
const numericAlign = convertStringAlign(align);
|
const numericAlign = convertStringAlign(align);
|
||||||
setStationAlign(station.id, numericAlign);
|
setStationAlign(station.id, numericAlign);
|
||||||
};
|
};
|
||||||
@@ -416,34 +390,29 @@ const StationLabel = observer(
|
|||||||
[position.x, position.y]
|
[position.x, position.y]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Функция для расчета позиции нижнего лейбла относительно ширины верхнего
|
|
||||||
const getSecondLabelPosition = (): number => {
|
const getSecondLabelPosition = (): number => {
|
||||||
if (!ruLabelWidth) return 0;
|
if (!ruLabelWidth) return 0;
|
||||||
|
|
||||||
switch (currentLabelAlign) {
|
switch (currentLabelAlign) {
|
||||||
case "left":
|
case "left":
|
||||||
// Позиционируем относительно левого края верхнего текста
|
|
||||||
return -ruLabelWidth / 2;
|
return -ruLabelWidth / 2;
|
||||||
case "center":
|
case "center":
|
||||||
// Центрируем относительно центра верхнего текста
|
|
||||||
return 0;
|
return 0;
|
||||||
case "right":
|
case "right":
|
||||||
// Позиционируем относительно правого края верхнего текста
|
|
||||||
return ruLabelWidth / 2;
|
return ruLabelWidth / 2;
|
||||||
default:
|
default:
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Функция для расчета anchor нижнего лейбла
|
|
||||||
const getSecondLabelAnchor = (): number => {
|
const getSecondLabelAnchor = (): number => {
|
||||||
switch (currentLabelAlign) {
|
switch (currentLabelAlign) {
|
||||||
case "left":
|
case "left":
|
||||||
return 0; // anchor.x = 0 (левый край)
|
return 0;
|
||||||
case "center":
|
case "center":
|
||||||
return 0.5; // anchor.x = 0.5 (центр)
|
return 0.5;
|
||||||
case "right":
|
case "right":
|
||||||
return 1; // anchor.x = 1 (правый край)
|
return 1;
|
||||||
default:
|
default:
|
||||||
return 0.5;
|
return 0.5;
|
||||||
}
|
}
|
||||||
@@ -522,10 +491,6 @@ const StationLabel = observer(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// =========================================================================
|
|
||||||
// Главный экспортируемый компонент: Станция
|
|
||||||
// =========================================================================
|
|
||||||
|
|
||||||
export const Station = ({
|
export const Station = ({
|
||||||
station,
|
station,
|
||||||
ruLabel,
|
ruLabel,
|
||||||
@@ -548,10 +513,9 @@ export const Station = ({
|
|||||||
|
|
||||||
g.circle(coordinates.x * UP_SCALE, coordinates.y * UP_SCALE, radius);
|
g.circle(coordinates.x * UP_SCALE, coordinates.y * UP_SCALE, radius);
|
||||||
|
|
||||||
// Change fill color when text is hovered
|
|
||||||
if (isTextHovered) {
|
if (isTextHovered) {
|
||||||
g.fill({ color: 0x00aaff }); // Highlight color when hovered
|
g.fill({ color: 0x00aaff });
|
||||||
g.stroke({ color: 0xffffff, width: strokeWidth + 1 }); // Brighter outline when hovered
|
g.stroke({ color: 0xffffff, width: strokeWidth + 1 });
|
||||||
} else {
|
} else {
|
||||||
g.fill({ color: PATH_COLOR });
|
g.fill({ color: PATH_COLOR });
|
||||||
g.stroke({ color: BACKGROUND_COLOR, width: strokeWidth });
|
g.stroke({ color: BACKGROUND_COLOR, width: strokeWidth });
|
||||||
|
|||||||
@@ -50,7 +50,6 @@ const TransformContext = createContext<{
|
|||||||
setScaleAtCenter: () => {},
|
setScaleAtCenter: () => {},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Provider component
|
|
||||||
export const TransformProvider = ({ children }: { children: ReactNode }) => {
|
export const TransformProvider = ({ children }: { children: ReactNode }) => {
|
||||||
const [position, setPosition] = useState({ x: 0, y: 0 });
|
const [position, setPosition] = useState({ x: 0, y: 0 });
|
||||||
const [scale, setScale] = useState(1);
|
const [scale, setScale] = useState(1);
|
||||||
@@ -59,12 +58,10 @@ export const TransformProvider = ({ children }: { children: ReactNode }) => {
|
|||||||
|
|
||||||
const screenToLocal = useCallback(
|
const screenToLocal = useCallback(
|
||||||
(screenX: number, screenY: number) => {
|
(screenX: number, screenY: number) => {
|
||||||
// Translate point relative to current pan position
|
|
||||||
const translatedX = (screenX - position.x) / scale;
|
const translatedX = (screenX - position.x) / scale;
|
||||||
const translatedY = (screenY - position.y) / scale;
|
const translatedY = (screenY - position.y) / scale;
|
||||||
|
|
||||||
// Rotate point around center
|
const cosRotation = Math.cos(-rotation);
|
||||||
const cosRotation = Math.cos(-rotation); // Negative rotation to reverse transform
|
|
||||||
const sinRotation = Math.sin(-rotation);
|
const sinRotation = Math.sin(-rotation);
|
||||||
const rotatedX = translatedX * cosRotation - translatedY * sinRotation;
|
const rotatedX = translatedX * cosRotation - translatedY * sinRotation;
|
||||||
const rotatedY = translatedX * sinRotation + translatedY * cosRotation;
|
const rotatedY = translatedX * sinRotation + translatedY * cosRotation;
|
||||||
@@ -77,7 +74,6 @@ export const TransformProvider = ({ children }: { children: ReactNode }) => {
|
|||||||
[position.x, position.y, scale, rotation]
|
[position.x, position.y, scale, rotation]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Inverse of screenToLocal
|
|
||||||
const localToScreen = useCallback(
|
const localToScreen = useCallback(
|
||||||
(localX: number, localY: number) => {
|
(localX: number, localY: number) => {
|
||||||
const upscaledX = localX * UP_SCALE;
|
const upscaledX = localX * UP_SCALE;
|
||||||
@@ -120,7 +116,6 @@ export const TransformProvider = ({ children }: { children: ReactNode }) => {
|
|||||||
(currentFromPosition.x - center.x) * sinDelta,
|
(currentFromPosition.x - center.x) * sinDelta,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Update both rotation and position in a single batch to avoid stale closure
|
|
||||||
setRotation(to);
|
setRotation(to);
|
||||||
setPosition(newPosition);
|
setPosition(newPosition);
|
||||||
},
|
},
|
||||||
@@ -150,13 +145,11 @@ export const TransformProvider = ({ children }: { children: ReactNode }) => {
|
|||||||
const cosRot = Math.cos(selectedRotation);
|
const cosRot = Math.cos(selectedRotation);
|
||||||
const sinRot = Math.sin(selectedRotation);
|
const sinRot = Math.sin(selectedRotation);
|
||||||
|
|
||||||
// Translate point relative to center, rotate, then translate back
|
|
||||||
const dx = newPosition.x;
|
const dx = newPosition.x;
|
||||||
const dy = newPosition.y;
|
const dy = newPosition.y;
|
||||||
newPosition.x = dx * cosRot - dy * sinRot + center.x;
|
newPosition.x = dx * cosRot - dy * sinRot + center.x;
|
||||||
newPosition.y = dx * sinRot + dy * cosRot + center.y;
|
newPosition.y = dx * sinRot + dy * cosRot + center.y;
|
||||||
|
|
||||||
// Batch state updates to avoid intermediate renders
|
|
||||||
setPosition(newPosition);
|
setPosition(newPosition);
|
||||||
setRotation(selectedRotation);
|
setRotation(selectedRotation);
|
||||||
setScale(selectedScale);
|
setScale(selectedScale);
|
||||||
@@ -184,7 +177,6 @@ export const TransformProvider = ({ children }: { children: ReactNode }) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const setScaleOnly = useCallback((newScale: number) => {
|
const setScaleOnly = useCallback((newScale: number) => {
|
||||||
// Изменяем только масштаб, не трогая позицию и поворот
|
|
||||||
setScale(newScale);
|
setScale(newScale);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -237,7 +229,6 @@ export const TransformProvider = ({ children }: { children: ReactNode }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Custom hook for easy access to transform values
|
|
||||||
export const useTransform = () => {
|
export const useTransform = () => {
|
||||||
const context = useContext(TransformContext);
|
const context = useContext(TransformContext);
|
||||||
if (!context) {
|
if (!context) {
|
||||||
|
|||||||
1725
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 * from "./SightListPage";
|
||||||
|
export { LinkedStations } from "./LinkedStations";
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ export const StationCreatePage = observer(() => {
|
|||||||
const { cities, getCities } = cityStore;
|
const { cities, getCities } = cityStore;
|
||||||
const { selectedCityId, selectedCity } = useSelectedCity();
|
const { selectedCityId, selectedCity } = useSelectedCity();
|
||||||
const [coordinates, setCoordinates] = useState<string>("");
|
const [coordinates, setCoordinates] = useState<string>("");
|
||||||
// НОВОЕ СОСТОЯНИЕ ДЛЯ ПРЕДУПРЕЖДАЮЩЕГО ОКНА
|
|
||||||
const [isSaveWarningOpen, setIsSaveWarningOpen] = useState(false);
|
const [isSaveWarningOpen, setIsSaveWarningOpen] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -49,7 +49,6 @@ export const StationCreatePage = observer(() => {
|
|||||||
}
|
}
|
||||||
}, [createStationData.common.latitude, createStationData.common.longitude]);
|
}, [createStationData.common.latitude, createStationData.common.longitude]);
|
||||||
|
|
||||||
// НОВАЯ ФУНКЦИЯ: Фактическое создание (вызывается после подтверждения)
|
|
||||||
const executeCreate = async () => {
|
const executeCreate = async () => {
|
||||||
try {
|
try {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
@@ -64,11 +63,13 @@ export const StationCreatePage = observer(() => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// ОБНОВЛЕННАЯ ФУНКЦИЯ: Проверка и вызов окна или создания
|
|
||||||
const handleCreate = async () => {
|
const handleCreate = async () => {
|
||||||
const isCityMissing = !createStationData.common.city_id;
|
const isCityMissing = !createStationData.common.city_id;
|
||||||
// Проверяем названия на всех языках
|
|
||||||
const isNameMissing = !createStationData.ru.name || !createStationData.en.name || !createStationData.zh.name;
|
const isNameMissing =
|
||||||
|
!createStationData.ru.name ||
|
||||||
|
!createStationData.en.name ||
|
||||||
|
!createStationData.zh.name;
|
||||||
|
|
||||||
if (isCityMissing || isNameMissing) {
|
if (isCityMissing || isNameMissing) {
|
||||||
setIsSaveWarningOpen(true);
|
setIsSaveWarningOpen(true);
|
||||||
@@ -78,13 +79,11 @@ export const StationCreatePage = observer(() => {
|
|||||||
await executeCreate();
|
await executeCreate();
|
||||||
};
|
};
|
||||||
|
|
||||||
// Обработчик "Да" в предупреждающем окне
|
|
||||||
const handleConfirmCreate = async () => {
|
const handleConfirmCreate = async () => {
|
||||||
setIsSaveWarningOpen(false);
|
setIsSaveWarningOpen(false);
|
||||||
await executeCreate();
|
await executeCreate();
|
||||||
};
|
};
|
||||||
|
|
||||||
// Обработчик "Нет" в предупреждающем окне
|
|
||||||
const handleCancelCreate = () => {
|
const handleCancelCreate = () => {
|
||||||
setIsSaveWarningOpen(false);
|
setIsSaveWarningOpen(false);
|
||||||
};
|
};
|
||||||
@@ -99,7 +98,6 @@ export const StationCreatePage = observer(() => {
|
|||||||
fetchCities();
|
fetchCities();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Автоматически устанавливаем выбранный город при загрузке страницы
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedCityId && selectedCity && !createStationData.common.city_id) {
|
if (selectedCityId && selectedCity && !createStationData.common.city_id) {
|
||||||
setCreateCommonData({
|
setCreateCommonData({
|
||||||
@@ -237,7 +235,7 @@ export const StationCreatePage = observer(() => {
|
|||||||
className="w-min flex gap-2 items-center"
|
className="w-min flex gap-2 items-center"
|
||||||
startIcon={<Save size={20} />}
|
startIcon={<Save size={20} />}
|
||||||
onClick={handleCreate}
|
onClick={handleCreate}
|
||||||
disabled={isLoading} // Убрал проверку name/city_id отсюда, чтобы ее обрабатывал handleCreate
|
disabled={isLoading}
|
||||||
>
|
>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<Loader2 size={20} className="animate-spin" />
|
<Loader2 size={20} className="animate-spin" />
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ export const StationEditPage = observer(() => {
|
|||||||
} = stationsStore;
|
} = stationsStore;
|
||||||
const { cities, getCities } = cityStore;
|
const { cities, getCities } = cityStore;
|
||||||
const [coordinates, setCoordinates] = useState<string>("");
|
const [coordinates, setCoordinates] = useState<string>("");
|
||||||
// НОВОЕ СОСТОЯНИЕ ДЛЯ ПРЕДУПРЕЖДАЮЩЕГО ОКНА
|
|
||||||
const [isSaveWarningOpen, setIsSaveWarningOpen] = useState(false);
|
const [isSaveWarningOpen, setIsSaveWarningOpen] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -50,7 +50,6 @@ export const StationEditPage = observer(() => {
|
|||||||
}
|
}
|
||||||
}, [editStationData.common.latitude, editStationData.common.longitude]);
|
}, [editStationData.common.latitude, editStationData.common.longitude]);
|
||||||
|
|
||||||
// НОВАЯ ФУНКЦИЯ: Фактическое редактирование (вызывается после подтверждения)
|
|
||||||
const executeEdit = async () => {
|
const executeEdit = async () => {
|
||||||
try {
|
try {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
@@ -64,10 +63,9 @@ export const StationEditPage = observer(() => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// ОБНОВЛЕННАЯ ФУНКЦИЯ: Проверка и вызов окна или редактирования
|
|
||||||
const handleEdit = async () => {
|
const handleEdit = async () => {
|
||||||
const isCityMissing = !editStationData.common.city_id;
|
const isCityMissing = !editStationData.common.city_id;
|
||||||
// Проверяем названия на всех языках
|
|
||||||
const isNameMissing =
|
const isNameMissing =
|
||||||
!editStationData.ru.name ||
|
!editStationData.ru.name ||
|
||||||
!editStationData.en.name ||
|
!editStationData.en.name ||
|
||||||
@@ -81,13 +79,11 @@ export const StationEditPage = observer(() => {
|
|||||||
await executeEdit();
|
await executeEdit();
|
||||||
};
|
};
|
||||||
|
|
||||||
// Обработчик "Да" в предупреждающем окне
|
|
||||||
const handleConfirmEdit = async () => {
|
const handleConfirmEdit = async () => {
|
||||||
setIsSaveWarningOpen(false);
|
setIsSaveWarningOpen(false);
|
||||||
await executeEdit();
|
await executeEdit();
|
||||||
};
|
};
|
||||||
|
|
||||||
// Обработчик "Нет" в предупреждающем окне
|
|
||||||
const handleCancelEdit = () => {
|
const handleCancelEdit = () => {
|
||||||
setIsSaveWarningOpen(false);
|
setIsSaveWarningOpen(false);
|
||||||
};
|
};
|
||||||
@@ -243,7 +239,7 @@ export const StationEditPage = observer(() => {
|
|||||||
className="w-min flex gap-2 items-center"
|
className="w-min flex gap-2 items-center"
|
||||||
startIcon={<Save size={20} />}
|
startIcon={<Save size={20} />}
|
||||||
onClick={handleEdit}
|
onClick={handleEdit}
|
||||||
disabled={isLoading} // Убрал проверку name/city_id отсюда, чтобы ее обрабатывал handleEdit
|
disabled={isLoading}
|
||||||
>
|
>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<Loader2 size={20} className="animate-spin" />
|
<Loader2 size={20} className="animate-spin" />
|
||||||
|
|||||||
@@ -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 |
@@ -8,15 +8,13 @@ import {
|
|||||||
Earth,
|
Earth,
|
||||||
Landmark,
|
Landmark,
|
||||||
GitBranch,
|
GitBranch,
|
||||||
// Car,
|
|
||||||
Table,
|
Table,
|
||||||
Split,
|
Split,
|
||||||
// Newspaper,
|
|
||||||
PersonStanding,
|
PersonStanding,
|
||||||
Cpu,
|
Cpu,
|
||||||
// BookImage,
|
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { CarrierSvg } from "./CarrierSvg";
|
|
||||||
|
import carrierIcon from "./carrier.svg";
|
||||||
|
|
||||||
export const DRAWER_WIDTH = 300;
|
export const DRAWER_WIDTH = 300;
|
||||||
|
|
||||||
@@ -56,12 +54,6 @@ export const NAVIGATION_ITEMS: {
|
|||||||
path: "/devices",
|
path: "/devices",
|
||||||
for_admin: true,
|
for_admin: true,
|
||||||
},
|
},
|
||||||
// {
|
|
||||||
// id: "vehicles",
|
|
||||||
// label: "Транспорт",
|
|
||||||
// icon: Car,
|
|
||||||
// path: "/vehicle",
|
|
||||||
// },
|
|
||||||
{
|
{
|
||||||
id: "users",
|
id: "users",
|
||||||
label: "Пользователи",
|
label: "Пользователи",
|
||||||
@@ -74,18 +66,6 @@ export const NAVIGATION_ITEMS: {
|
|||||||
label: "Справочник",
|
label: "Справочник",
|
||||||
icon: Table,
|
icon: Table,
|
||||||
nestedItems: [
|
nestedItems: [
|
||||||
// {
|
|
||||||
// id: "media",
|
|
||||||
// label: "Медиа",
|
|
||||||
// icon: BookImage,
|
|
||||||
// path: "/media",
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// id: "articles",
|
|
||||||
// label: "Статьи",
|
|
||||||
// icon: Newspaper,
|
|
||||||
// path: "/article",
|
|
||||||
// },
|
|
||||||
{
|
{
|
||||||
id: "attractions",
|
id: "attractions",
|
||||||
label: "Достопримечательности",
|
label: "Достопримечательности",
|
||||||
@@ -123,7 +103,7 @@ export const NAVIGATION_ITEMS: {
|
|||||||
id: "carriers",
|
id: "carriers",
|
||||||
label: "Перевозчики",
|
label: "Перевозчики",
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
icon: CarrierSvg,
|
icon: () => <img src={carrierIcon} alt="Перевозчики" />,
|
||||||
path: "/carrier",
|
path: "/carrier",
|
||||||
for_admin: true,
|
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 API_URL = import.meta.env.VITE_API_URL;
|
||||||
|
|
||||||
export const MEDIA_TYPE_LABELS = {
|
export const MEDIA_TYPE_LABELS = {
|
||||||
1: "Фото",
|
1: "Фото",
|
||||||
2: "Видео",
|
2: "Видео",
|
||||||
@@ -22,751 +40,6 @@ export const MEDIA_TYPE_VALUES = {
|
|||||||
video_preview: 2,
|
video_preview: 2,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const RU_COUNTRIES = [
|
export const RU_COUNTRIES = generateCountriesList("ru");
|
||||||
{ code: "AF", name: "Афганистан" },
|
export const EN_COUNTRIES = generateCountriesList("en");
|
||||||
{ code: "AX", name: "Аландские острова" },
|
export const ZH_COUNTRIES = generateCountriesList("zh");
|
||||||
{ 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: "津巴布韦" },
|
|
||||||
];
|
|
||||||
@@ -1,8 +1,3 @@
|
|||||||
/**
|
|
||||||
* Утилита для управления кешем GLTF и blob URL
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Динамический импорт useGLTF для избежания проблем с SSR
|
|
||||||
let useGLTF: any = null;
|
let useGLTF: any = null;
|
||||||
|
|
||||||
const initializeUseGLTF = async () => {
|
const initializeUseGLTF = async () => {
|
||||||
@@ -20,9 +15,6 @@ const initializeUseGLTF = async () => {
|
|||||||
return useGLTF;
|
return useGLTF;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Очищает кеш GLTF для конкретного URL
|
|
||||||
*/
|
|
||||||
export const clearGLTFCacheForUrl = async (url: string) => {
|
export const clearGLTFCacheForUrl = async (url: string) => {
|
||||||
try {
|
try {
|
||||||
const gltf = await initializeUseGLTF();
|
const gltf = await initializeUseGLTF();
|
||||||
@@ -32,9 +24,6 @@ export const clearGLTFCacheForUrl = async (url: string) => {
|
|||||||
} catch (error) {}
|
} catch (error) {}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Очищает весь кеш GLTF
|
|
||||||
*/
|
|
||||||
export const clearAllGLTFCache = async () => {
|
export const clearAllGLTFCache = async () => {
|
||||||
try {
|
try {
|
||||||
const gltf = await initializeUseGLTF();
|
const gltf = await initializeUseGLTF();
|
||||||
@@ -44,9 +33,6 @@ export const clearAllGLTFCache = async () => {
|
|||||||
} catch (error) {}
|
} catch (error) {}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Очищает blob URL из памяти браузера
|
|
||||||
*/
|
|
||||||
export const revokeBlobURL = (url: string) => {
|
export const revokeBlobURL = (url: string) => {
|
||||||
if (url && url.startsWith("blob:")) {
|
if (url && url.startsWith("blob:")) {
|
||||||
try {
|
try {
|
||||||
@@ -55,27 +41,16 @@ export const revokeBlobURL = (url: string) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Комплексная очистка: blob URL + кеш GLTF
|
|
||||||
*/
|
|
||||||
export const clearBlobAndGLTFCache = async (url: string) => {
|
export const clearBlobAndGLTFCache = async (url: string) => {
|
||||||
// Сначала отзываем blob URL
|
|
||||||
revokeBlobURL(url);
|
revokeBlobURL(url);
|
||||||
|
|
||||||
// Затем очищаем кеш GLTF
|
|
||||||
await clearGLTFCacheForUrl(url);
|
await clearGLTFCacheForUrl(url);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Очистка при смене медиа (для предотвращения конфликтов)
|
|
||||||
*/
|
|
||||||
export const clearMediaTransitionCache = async (
|
export const clearMediaTransitionCache = async (
|
||||||
previousMediaId: string | number | null,
|
previousMediaId: string | number | null,
|
||||||
newMediaId: string | number | null,
|
|
||||||
newMediaType?: number
|
newMediaType?: number
|
||||||
) => {
|
) => {
|
||||||
console.log(newMediaId, newMediaType);
|
|
||||||
// Если переключаемся с/на 3D модель, очищаем весь кеш
|
|
||||||
if (newMediaType === 6 || previousMediaId) {
|
if (newMediaType === 6 || previousMediaId) {
|
||||||
await clearAllGLTFCache();
|
await clearAllGLTFCache();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,34 +2,17 @@ export * from "./mui/theme";
|
|||||||
export * from "./DecodeJWT";
|
export * from "./DecodeJWT";
|
||||||
export * from "./gltfCacheManager";
|
export * from "./gltfCacheManager";
|
||||||
|
|
||||||
/**
|
|
||||||
* Генерирует название медиа по умолчанию в разных форматах
|
|
||||||
*
|
|
||||||
* Примеры использования:
|
|
||||||
* - Для достопримечательности с названием: "Михайловский замок_mikhail-zamok_Фото"
|
|
||||||
* - Для достопримечательности без названия: "Название_mikhail-zamok_Фото"
|
|
||||||
* - Для статьи: "Михайловский замок_mikhail-zamok_Факты" (где "Факты" - название статьи)
|
|
||||||
*
|
|
||||||
* @param objectName - Название объекта (достопримечательности, города и т.д.)
|
|
||||||
* @param fileName - Название файла
|
|
||||||
* @param mediaType - Тип медиа (число) или название статьи
|
|
||||||
* @param isArticle - Флаг, указывающий что медиа добавляется к статье
|
|
||||||
* @returns Строка в нужном формате
|
|
||||||
*/
|
|
||||||
export const generateDefaultMediaName = (
|
export const generateDefaultMediaName = (
|
||||||
objectName: string,
|
objectName: string,
|
||||||
fileName: string,
|
fileName: string,
|
||||||
mediaType: number | string,
|
mediaType: number | string,
|
||||||
isArticle: boolean = false
|
isArticle: boolean = false
|
||||||
): string => {
|
): string => {
|
||||||
// Убираем расширение из названия файла
|
|
||||||
const fileNameWithoutExtension = fileName.split(".").slice(0, -1).join(".");
|
const fileNameWithoutExtension = fileName.split(".").slice(0, -1).join(".");
|
||||||
|
|
||||||
if (isArticle && typeof mediaType === "string") {
|
if (isArticle && typeof mediaType === "string") {
|
||||||
// Для статей: "Название достопримечательности_название файла_название статьи"
|
|
||||||
return `${objectName}_${fileNameWithoutExtension}_${mediaType}`;
|
return `${objectName}_${fileNameWithoutExtension}_${mediaType}`;
|
||||||
} else if (typeof mediaType === "number") {
|
} else if (typeof mediaType === "number") {
|
||||||
// Получаем название типа медиа
|
|
||||||
const mediaTypeLabels: Record<number, string> = {
|
const mediaTypeLabels: Record<number, string> = {
|
||||||
1: "Фото",
|
1: "Фото",
|
||||||
2: "Видео",
|
2: "Видео",
|
||||||
@@ -42,14 +25,11 @@ export const generateDefaultMediaName = (
|
|||||||
const mediaTypeLabel = mediaTypeLabels[mediaType] || "Медиа";
|
const mediaTypeLabel = mediaTypeLabels[mediaType] || "Медиа";
|
||||||
|
|
||||||
if (objectName && objectName.trim() !== "") {
|
if (objectName && objectName.trim() !== "") {
|
||||||
// Если есть название объекта: "Название объекта_название файла_тип медиа"
|
|
||||||
return `${objectName}_${fileNameWithoutExtension}_${mediaTypeLabel}`;
|
return `${objectName}_${fileNameWithoutExtension}_${mediaTypeLabel}`;
|
||||||
} else {
|
} else {
|
||||||
// Если нет названия объекта: "Название_название файла_тип медиа"
|
|
||||||
return `Название_${fileNameWithoutExtension}_${mediaTypeLabel}`;
|
return `Название_${fileNameWithoutExtension}_${mediaTypeLabel}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback
|
|
||||||
return `${objectName || "Название"}_${fileNameWithoutExtension}_Медиа`;
|
return `${objectName || "Название"}_${fileNameWithoutExtension}_Медиа`;
|
||||||
};
|
};
|
||||||
|
|||||||
1067
src/shared/modals/ArticleSelectOrCreateDialog/index.tsx
Normal file
@@ -54,7 +54,7 @@ interface UploadMediaDialogProps {
|
|||||||
| "station";
|
| "station";
|
||||||
isArticle?: boolean;
|
isArticle?: boolean;
|
||||||
articleName?: string;
|
articleName?: string;
|
||||||
initialFile?: File; // <--- добавлено
|
initialFile?: File;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const UploadMediaDialog = observer(
|
export const UploadMediaDialog = observer(
|
||||||
@@ -68,7 +68,7 @@ export const UploadMediaDialog = observer(
|
|||||||
|
|
||||||
isArticle,
|
isArticle,
|
||||||
articleName,
|
articleName,
|
||||||
initialFile, // <--- добавлено
|
initialFile,
|
||||||
}: UploadMediaDialogProps) => {
|
}: UploadMediaDialogProps) => {
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
@@ -87,7 +87,6 @@ export const UploadMediaDialog = observer(
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (initialFile) {
|
if (initialFile) {
|
||||||
// Очищаем предыдущий blob URL если он существует
|
|
||||||
if (
|
if (
|
||||||
previousMediaUrlRef.current &&
|
previousMediaUrlRef.current &&
|
||||||
previousMediaUrlRef.current.startsWith("blob:")
|
previousMediaUrlRef.current.startsWith("blob:")
|
||||||
@@ -106,7 +105,6 @@ export const UploadMediaDialog = observer(
|
|||||||
}
|
}
|
||||||
}, [initialFile]);
|
}, [initialFile]);
|
||||||
|
|
||||||
// Очистка blob URL при размонтировании компонента
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
if (
|
if (
|
||||||
@@ -116,13 +114,13 @@ export const UploadMediaDialog = observer(
|
|||||||
clearBlobAndGLTFCache(previousMediaUrlRef.current);
|
clearBlobAndGLTFCache(previousMediaUrlRef.current);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, []); // Пустой массив зависимостей - выполняется только при размонтировании
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (fileToUpload) {
|
if (fileToUpload) {
|
||||||
setMediaFile(fileToUpload);
|
setMediaFile(fileToUpload);
|
||||||
setMediaFilename(fileToUpload.name);
|
setMediaFilename(fileToUpload.name);
|
||||||
// Try to determine media type from file extension
|
|
||||||
const extension = fileToUpload.name.split(".").pop()?.toLowerCase();
|
const extension = fileToUpload.name.split(".").pop()?.toLowerCase();
|
||||||
if (extension) {
|
if (extension) {
|
||||||
if (["glb", "gltf"].includes(extension)) {
|
if (["glb", "gltf"].includes(extension)) {
|
||||||
@@ -134,22 +132,18 @@ export const UploadMediaDialog = observer(
|
|||||||
extension
|
extension
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
// Для изображений доступны все типы кроме видео
|
setAvailableMediaTypes([1, 3, 4, 5]);
|
||||||
setAvailableMediaTypes([1, 3, 4, 5]); // Фото, Иконка, Водяной знак, Панорама, 3Д-модель
|
setMediaType(1);
|
||||||
setMediaType(1); // По умолчанию Фото
|
|
||||||
} else if (["mp4", "webm", "mov"].includes(extension)) {
|
} else if (["mp4", "webm", "mov"].includes(extension)) {
|
||||||
// Для видео только тип Видео
|
|
||||||
setAvailableMediaTypes([2]);
|
setAvailableMediaTypes([2]);
|
||||||
setMediaType(2);
|
setMediaType(2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Генерируем название по умолчанию если есть контекст
|
|
||||||
if (fileToUpload.name) {
|
if (fileToUpload.name) {
|
||||||
let defaultName = "";
|
let defaultName = "";
|
||||||
|
|
||||||
if (isArticle && articleName && contextObjectName) {
|
if (isArticle && articleName && contextObjectName) {
|
||||||
// Для статей: "Название достопримечательности_название файла_название статьи"
|
|
||||||
defaultName = generateDefaultMediaName(
|
defaultName = generateDefaultMediaName(
|
||||||
contextObjectName,
|
contextObjectName,
|
||||||
fileToUpload.name,
|
fileToUpload.name,
|
||||||
@@ -157,10 +151,9 @@ export const UploadMediaDialog = observer(
|
|||||||
true
|
true
|
||||||
);
|
);
|
||||||
} else if (contextObjectName && contextObjectName.trim() !== "") {
|
} else if (contextObjectName && contextObjectName.trim() !== "") {
|
||||||
// Для обычных медиа с названием объекта
|
|
||||||
const currentMediaType = hardcodeType
|
const currentMediaType = hardcodeType
|
||||||
? MEDIA_TYPE_VALUES[hardcodeType]
|
? MEDIA_TYPE_VALUES[hardcodeType]
|
||||||
: 1; // По умолчанию фото
|
: 1;
|
||||||
defaultName = generateDefaultMediaName(
|
defaultName = generateDefaultMediaName(
|
||||||
contextObjectName,
|
contextObjectName,
|
||||||
fileToUpload.name,
|
fileToUpload.name,
|
||||||
@@ -168,10 +161,9 @@ export const UploadMediaDialog = observer(
|
|||||||
false
|
false
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// Для медиа без названия объекта
|
|
||||||
const currentMediaType = hardcodeType
|
const currentMediaType = hardcodeType
|
||||||
? MEDIA_TYPE_VALUES[hardcodeType]
|
? MEDIA_TYPE_VALUES[hardcodeType]
|
||||||
: 1; // По умолчанию фото
|
: 1;
|
||||||
defaultName = generateDefaultMediaName(
|
defaultName = generateDefaultMediaName(
|
||||||
"",
|
"",
|
||||||
fileToUpload.name,
|
fileToUpload.name,
|
||||||
@@ -185,13 +177,11 @@ export const UploadMediaDialog = observer(
|
|||||||
}
|
}
|
||||||
}, [fileToUpload, contextObjectName, hardcodeType, isArticle, articleName]);
|
}, [fileToUpload, contextObjectName, hardcodeType, isArticle, articleName]);
|
||||||
|
|
||||||
// Обновляем название при изменении типа медиа
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (mediaFilename && mediaType > 0) {
|
if (mediaFilename && mediaType > 0) {
|
||||||
let defaultName = "";
|
let defaultName = "";
|
||||||
|
|
||||||
if (isArticle && articleName && contextObjectName) {
|
if (isArticle && articleName && contextObjectName) {
|
||||||
// Для статей: "Название достопримечательности_название файла_название статьи"
|
|
||||||
defaultName = generateDefaultMediaName(
|
defaultName = generateDefaultMediaName(
|
||||||
contextObjectName,
|
contextObjectName,
|
||||||
mediaFilename,
|
mediaFilename,
|
||||||
@@ -199,7 +189,6 @@ export const UploadMediaDialog = observer(
|
|||||||
true
|
true
|
||||||
);
|
);
|
||||||
} else if (contextObjectName && contextObjectName.trim() !== "") {
|
} else if (contextObjectName && contextObjectName.trim() !== "") {
|
||||||
// Для обычных медиа с названием объекта
|
|
||||||
const currentMediaType = hardcodeType
|
const currentMediaType = hardcodeType
|
||||||
? MEDIA_TYPE_VALUES[hardcodeType]
|
? MEDIA_TYPE_VALUES[hardcodeType]
|
||||||
: mediaType;
|
: mediaType;
|
||||||
@@ -210,7 +199,6 @@ export const UploadMediaDialog = observer(
|
|||||||
false
|
false
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// Для медиа без названия объекта
|
|
||||||
const currentMediaType = hardcodeType
|
const currentMediaType = hardcodeType
|
||||||
? MEDIA_TYPE_VALUES[hardcodeType]
|
? MEDIA_TYPE_VALUES[hardcodeType]
|
||||||
: mediaType;
|
: mediaType;
|
||||||
@@ -235,7 +223,6 @@ export const UploadMediaDialog = observer(
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (mediaFile) {
|
if (mediaFile) {
|
||||||
// Очищаем предыдущий blob URL и кеш GLTF если он существует
|
|
||||||
if (
|
if (
|
||||||
previousMediaUrlRef.current &&
|
previousMediaUrlRef.current &&
|
||||||
previousMediaUrlRef.current.startsWith("blob:")
|
previousMediaUrlRef.current.startsWith("blob:")
|
||||||
@@ -245,22 +232,10 @@ export const UploadMediaDialog = observer(
|
|||||||
|
|
||||||
const newBlobUrl = URL.createObjectURL(mediaFile as Blob);
|
const newBlobUrl = URL.createObjectURL(mediaFile as Blob);
|
||||||
setMediaUrl(newBlobUrl);
|
setMediaUrl(newBlobUrl);
|
||||||
previousMediaUrlRef.current = newBlobUrl; // Сохраняем новый URL в ref
|
previousMediaUrlRef.current = newBlobUrl;
|
||||||
setIsPreviewLoaded(false); // Сбрасываем состояние загрузки при смене файла
|
setIsPreviewLoaded(false);
|
||||||
}
|
}
|
||||||
}, [mediaFile]); // Убираем mediaUrl из зависимостей чтобы избежать зацикливания
|
}, [mediaFile]);
|
||||||
|
|
||||||
// const fileFormat = useEffect(() => {
|
|
||||||
// const handleKeyPress = (event: KeyboardEvent) => {
|
|
||||||
// if (event.key.toLowerCase() === "enter" && !event.ctrlKey) {
|
|
||||||
// event.preventDefault();
|
|
||||||
// onClose();
|
|
||||||
// }
|
|
||||||
// };
|
|
||||||
|
|
||||||
// window.addEventListener("keydown", handleKeyPress);
|
|
||||||
// return () => window.removeEventListener("keydown", handleKeyPress);
|
|
||||||
// }, [onClose]);
|
|
||||||
|
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
if (!mediaFile) return;
|
if (!mediaFile) return;
|
||||||
@@ -285,10 +260,10 @@ export const UploadMediaDialog = observer(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
setSuccess(true);
|
setSuccess(true);
|
||||||
// Закрываем модальное окно после успешного сохранения
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
handleClose();
|
handleClose();
|
||||||
}, 1000); // Небольшая задержка, чтобы пользователь увидел сообщение об успехе
|
}, 1000);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err.message : "Failed to save media");
|
setError(err instanceof Error ? err.message : "Failed to save media");
|
||||||
} finally {
|
} finally {
|
||||||
@@ -297,7 +272,6 @@ export const UploadMediaDialog = observer(
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
// Очищаем blob URL и кеш GLTF при закрытии диалога
|
|
||||||
if (
|
if (
|
||||||
previousMediaUrlRef.current &&
|
previousMediaUrlRef.current &&
|
||||||
previousMediaUrlRef.current.startsWith("blob:")
|
previousMediaUrlRef.current.startsWith("blob:")
|
||||||
@@ -310,7 +284,7 @@ export const UploadMediaDialog = observer(
|
|||||||
setMediaUrl(null);
|
setMediaUrl(null);
|
||||||
setMediaFile(null);
|
setMediaFile(null);
|
||||||
setIsPreviewLoaded(false);
|
setIsPreviewLoaded(false);
|
||||||
previousMediaUrlRef.current = null; // Очищаем ref
|
previousMediaUrlRef.current = null;
|
||||||
onClose();
|
onClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -2,3 +2,4 @@ export * from "./SelectArticleDialog";
|
|||||||
export * from "./SelectMediaDialog";
|
export * from "./SelectMediaDialog";
|
||||||
export * from "./PreviewMediaDialog";
|
export * from "./PreviewMediaDialog";
|
||||||
export * from "./UploadMediaDialog";
|
export * from "./UploadMediaDialog";
|
||||||
|
export * from "./ArticleSelectOrCreateDialog";
|
||||||
|
|||||||
@@ -171,7 +171,6 @@ class CarrierStore {
|
|||||||
this.carriers[language].data.push(response.data);
|
this.carriers[language].data.push(response.data);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create translations for other languages
|
|
||||||
for (const lang of ["ru", "en", "zh"].filter((l) => l !== language)) {
|
for (const lang of ["ru", "en", "zh"].filter((l) => l !== language)) {
|
||||||
const patchPayload = {
|
const patchPayload = {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
// @shared/stores/createSightStore.ts
|
|
||||||
import {
|
import {
|
||||||
articlesStore,
|
articlesStore,
|
||||||
Language,
|
Language,
|
||||||
@@ -27,7 +26,6 @@ type SightLanguageInfo = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
type SightCommonInfo = {
|
type SightCommonInfo = {
|
||||||
// id: number; // ID is 0 until created
|
|
||||||
city_id: number;
|
city_id: number;
|
||||||
city: string;
|
city: string;
|
||||||
latitude: number;
|
latitude: number;
|
||||||
@@ -35,13 +33,11 @@ type SightCommonInfo = {
|
|||||||
thumbnail: string | null;
|
thumbnail: string | null;
|
||||||
watermark_lu: string | null;
|
watermark_lu: string | null;
|
||||||
watermark_rd: string | null;
|
watermark_rd: string | null;
|
||||||
left_article: number; // Can be 0 or a real ID, or placeholder like 10000000
|
left_article: number;
|
||||||
preview_media: string | null;
|
preview_media: string | null;
|
||||||
video_preview: string | null;
|
video_preview: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
// SightBaseInfo combines common info with language-specific info
|
|
||||||
// The 'id' for the sight itself will be assigned upon creation by the backend.
|
|
||||||
type SightBaseInfo = SightCommonInfo & {
|
type SightBaseInfo = SightCommonInfo & {
|
||||||
[key in Language]: SightLanguageInfo;
|
[key in Language]: SightLanguageInfo;
|
||||||
};
|
};
|
||||||
@@ -78,7 +74,7 @@ const initialSightState: SightBaseInfo = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
class CreateSightStore {
|
class CreateSightStore {
|
||||||
sight: SightBaseInfo = JSON.parse(JSON.stringify(initialSightState)); // Deep copy for reset
|
sight: SightBaseInfo = JSON.parse(JSON.stringify(initialSightState));
|
||||||
|
|
||||||
uploadMediaOpen = false;
|
uploadMediaOpen = false;
|
||||||
setUploadMediaOpen = (open: boolean) => {
|
setUploadMediaOpen = (open: boolean) => {
|
||||||
@@ -93,9 +89,7 @@ class CreateSightStore {
|
|||||||
makeAutoObservable(this);
|
makeAutoObservable(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Right Article Management ---
|
|
||||||
createNewRightArticle = async () => {
|
createNewRightArticle = async () => {
|
||||||
// Create article in DB for all languages
|
|
||||||
const articleRuData = {
|
const articleRuData = {
|
||||||
heading: "Новый заголовок (RU)",
|
heading: "Новый заголовок (RU)",
|
||||||
body: "Новый текст (RU)",
|
body: "Новый текст (RU)",
|
||||||
@@ -125,7 +119,7 @@ class CreateSightStore {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const { id } = articleRes.data; // New article's ID
|
const { id } = articleRes.data;
|
||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
const newArticleEntry = { id, media: [] };
|
const newArticleEntry = { id, media: [] };
|
||||||
@@ -133,7 +127,7 @@ class CreateSightStore {
|
|||||||
this.sight.en.right.push({ ...newArticleEntry, ...articleEnData });
|
this.sight.en.right.push({ ...newArticleEntry, ...articleEnData });
|
||||||
this.sight.zh.right.push({ ...newArticleEntry, ...articleZhData });
|
this.sight.zh.right.push({ ...newArticleEntry, ...articleZhData });
|
||||||
});
|
});
|
||||||
return id; // Return ID for potential immediate use
|
return id;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error creating new right article:", error);
|
console.error("Error creating new right article:", error);
|
||||||
throw error;
|
throw error;
|
||||||
@@ -169,7 +163,7 @@ class CreateSightStore {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
return articleId; // Return the linked article ID
|
return articleId;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error linking existing right article:", error);
|
console.error("Error linking existing right article:", error);
|
||||||
throw error;
|
throw error;
|
||||||
@@ -188,9 +182,7 @@ class CreateSightStore {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// "Unlink" in create mode means just removing from the list to be created with the sight
|
|
||||||
unlinkRightAritcle = (articleId: number) => {
|
unlinkRightAritcle = (articleId: number) => {
|
||||||
// Changed from 'unlinkRightAritcle' spelling
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.sight.ru.right = this.sight.ru.right.filter(
|
this.sight.ru.right = this.sight.ru.right.filter(
|
||||||
(article) => article.id !== articleId
|
(article) => article.id !== articleId
|
||||||
@@ -202,16 +194,12 @@ class CreateSightStore {
|
|||||||
(article) => article.id !== articleId
|
(article) => article.id !== articleId
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
// Note: If this article was created via createNewRightArticle, it still exists in the DB.
|
|
||||||
// Consider if an orphaned article should be deleted here or managed separately.
|
|
||||||
// For now, it just removes it from the list associated with *this specific sight creation process*.
|
|
||||||
};
|
};
|
||||||
|
|
||||||
deleteRightArticle = async (articleId: number) => {
|
deleteRightArticle = async (articleId: number) => {
|
||||||
try {
|
try {
|
||||||
await authInstance.delete(`/article/${articleId}`); // Delete from backend
|
await authInstance.delete(`/article/${articleId}`);
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
// Remove from local store for all languages
|
|
||||||
this.sight.ru.right = this.sight.ru.right.filter(
|
this.sight.ru.right = this.sight.ru.right.filter(
|
||||||
(article) => article.id !== articleId
|
(article) => article.id !== articleId
|
||||||
);
|
);
|
||||||
@@ -228,12 +216,11 @@ class CreateSightStore {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- Right Article Media Management ---
|
|
||||||
createLinkWithRightArticle = async (media: MediaItem, articleId: number) => {
|
createLinkWithRightArticle = async (media: MediaItem, articleId: number) => {
|
||||||
try {
|
try {
|
||||||
await authInstance.post(`/article/${articleId}/media`, {
|
await authInstance.post(`/article/${articleId}/media`, {
|
||||||
media_id: media.id,
|
media_id: media.id,
|
||||||
media_order: 1, // Or calculate based on existing media.length + 1
|
media_order: 1,
|
||||||
});
|
});
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
(["ru", "en", "zh"] as Language[]).forEach((lang) => {
|
(["ru", "en", "zh"] as Language[]).forEach((lang) => {
|
||||||
@@ -242,7 +229,7 @@ class CreateSightStore {
|
|||||||
);
|
);
|
||||||
if (article) {
|
if (article) {
|
||||||
if (!article.media) article.media = [];
|
if (!article.media) article.media = [];
|
||||||
article.media.unshift(media); // Add to the beginning
|
article.media.unshift(media);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -273,7 +260,6 @@ class CreateSightStore {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- Left Article Management (largely unchanged from your provided store) ---
|
|
||||||
updateLeftInfo = (language: Language, heading: string, body: string) => {
|
updateLeftInfo = (language: Language, heading: string, body: string) => {
|
||||||
this.sight[language].left.heading = heading;
|
this.sight[language].left.heading = heading;
|
||||||
this.sight[language].left.body = body;
|
this.sight[language].left.body = body;
|
||||||
@@ -323,7 +309,7 @@ class CreateSightStore {
|
|||||||
deleteLeftArticle = async (articleId: number) => {
|
deleteLeftArticle = async (articleId: number) => {
|
||||||
/* ... your existing logic ... */
|
/* ... your existing logic ... */
|
||||||
await authInstance.delete(`/article/${articleId}`);
|
await authInstance.delete(`/article/${articleId}`);
|
||||||
// articlesStore.getArticles(languageStore.language); // If still neede
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
articlesStore.articles.ru = articlesStore.articles.ru.filter(
|
articlesStore.articles.ru = articlesStore.articles.ru.filter(
|
||||||
(article) => article.id !== articleId
|
(article) => article.id !== articleId
|
||||||
@@ -340,63 +326,69 @@ class CreateSightStore {
|
|||||||
|
|
||||||
createLeftArticle = async () => {
|
createLeftArticle = async () => {
|
||||||
/* ... your existing logic to create a new left article (placeholder or DB) ... */
|
/* ... 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();
|
||||||
|
|
||||||
|
const hasAnyName = !!(ruName || enName || zhName);
|
||||||
|
|
||||||
const response = await languageInstance("ru").post("/article", {
|
const response = await languageInstance("ru").post("/article", {
|
||||||
heading: "Новая левая статья",
|
heading: hasAnyName ? ruName : "",
|
||||||
body: "Заполните контентом",
|
body: "",
|
||||||
});
|
});
|
||||||
const newLeftArticleId = response.data.id;
|
const newLeftArticleId = response.data.id;
|
||||||
|
|
||||||
await languageInstance("en").patch(`/article/${newLeftArticleId}`, {
|
await languageInstance("en").patch(`/article/${newLeftArticleId}`, {
|
||||||
heading: "New Left Article",
|
heading: hasAnyName ? enName : "",
|
||||||
body: "Fill with content",
|
body: "",
|
||||||
});
|
});
|
||||||
await languageInstance("zh").patch(`/article/${newLeftArticleId}`, {
|
await languageInstance("zh").patch(`/article/${newLeftArticleId}`, {
|
||||||
heading: "新的左侧文章",
|
heading: hasAnyName ? zhName : "",
|
||||||
body: "填写内容",
|
body: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.sight.left_article = newLeftArticleId; // Store the actual ID
|
this.sight.left_article = newLeftArticleId;
|
||||||
this.sight.ru.left = {
|
this.sight.ru.left = {
|
||||||
heading: "Новая левая статья",
|
heading: hasAnyName ? ruName : "",
|
||||||
body: "Заполните контентом",
|
body: "",
|
||||||
media: [],
|
media: [],
|
||||||
};
|
};
|
||||||
this.sight.en.left = {
|
this.sight.en.left = {
|
||||||
heading: "New Left Article",
|
heading: hasAnyName ? enName : "",
|
||||||
body: "Fill with content",
|
body: "",
|
||||||
media: [],
|
media: [],
|
||||||
};
|
};
|
||||||
this.sight.zh.left = {
|
this.sight.zh.left = {
|
||||||
heading: "新的左侧文章",
|
heading: hasAnyName ? zhName : "",
|
||||||
body: "填写内容",
|
body: "",
|
||||||
media: [],
|
media: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
articlesStore.articles.ru.push({
|
articlesStore.articles.ru.push({
|
||||||
id: newLeftArticleId,
|
id: newLeftArticleId,
|
||||||
heading: "Новая левая статья",
|
heading: hasAnyName ? ruName : "",
|
||||||
body: "Заполните контентом",
|
body: "",
|
||||||
service_name: "Новая левая статья",
|
service_name: hasAnyName ? ruName : "",
|
||||||
});
|
});
|
||||||
articlesStore.articles.en.push({
|
articlesStore.articles.en.push({
|
||||||
id: newLeftArticleId,
|
id: newLeftArticleId,
|
||||||
heading: "New Left Article",
|
heading: hasAnyName ? enName : "",
|
||||||
body: "Fill with content",
|
body: "",
|
||||||
service_name: "New Left Article",
|
service_name: hasAnyName ? enName : "",
|
||||||
});
|
});
|
||||||
articlesStore.articles.zh.push({
|
articlesStore.articles.zh.push({
|
||||||
id: newLeftArticleId,
|
id: newLeftArticleId,
|
||||||
heading: "新的左侧文章",
|
heading: hasAnyName ? zhName : "",
|
||||||
body: "填写内容",
|
body: "",
|
||||||
service_name: "新的左侧文章",
|
service_name: hasAnyName ? zhName : "",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
return newLeftArticleId;
|
return newLeftArticleId;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Placeholder for a "new" unsaved left article
|
|
||||||
setNewLeftArticlePlaceholder = () => {
|
setNewLeftArticlePlaceholder = () => {
|
||||||
this.sight.left_article = 10000000; // Special placeholder ID
|
this.sight.left_article = 10000000;
|
||||||
this.sight.ru.left = {
|
this.sight.ru.left = {
|
||||||
heading: "Новая левая статья",
|
heading: "Новая левая статья",
|
||||||
body: "Заполните контентом",
|
body: "Заполните контентом",
|
||||||
@@ -414,7 +406,6 @@ class CreateSightStore {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- Sight Preview Media ---
|
|
||||||
linkPreviewMedia = (mediaId: string) => {
|
linkPreviewMedia = (mediaId: string) => {
|
||||||
this.sight.preview_media = mediaId;
|
this.sight.preview_media = mediaId;
|
||||||
};
|
};
|
||||||
@@ -423,32 +414,27 @@ class CreateSightStore {
|
|||||||
this.sight.preview_media = null;
|
this.sight.preview_media = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- General Store Methods ---
|
|
||||||
clearCreateSight = () => {
|
clearCreateSight = () => {
|
||||||
this.needLeaveAgree = false;
|
this.needLeaveAgree = false;
|
||||||
this.sight = JSON.parse(JSON.stringify(initialSightState)); // Reset to initial
|
this.sight = JSON.parse(JSON.stringify(initialSightState));
|
||||||
};
|
};
|
||||||
|
|
||||||
updateSightInfo = (
|
updateSightInfo = (
|
||||||
content: Partial<SightLanguageInfo | SightCommonInfo>, // Corrected types
|
content: Partial<SightLanguageInfo | SightCommonInfo>,
|
||||||
language?: Language
|
language?: Language
|
||||||
) => {
|
) => {
|
||||||
this.needLeaveAgree = true;
|
this.needLeaveAgree = true;
|
||||||
if (language) {
|
if (language) {
|
||||||
this.sight[language] = { ...this.sight[language], ...content };
|
this.sight[language] = { ...this.sight[language], ...content };
|
||||||
} else {
|
} else {
|
||||||
// Assuming content here is for SightCommonInfo
|
|
||||||
this.sight = { ...this.sight, ...(content as Partial<SightCommonInfo>) };
|
this.sight = { ...this.sight, ...(content as Partial<SightCommonInfo>) };
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- Main Sight Creation Logic ---
|
|
||||||
createSight = async (primaryLanguage: Language) => {
|
createSight = async (primaryLanguage: Language) => {
|
||||||
let finalLeftArticleId = this.sight.left_article;
|
let finalLeftArticleId = this.sight.left_article;
|
||||||
|
|
||||||
// 1. Handle Left Article (Create if new, or use existing ID)
|
|
||||||
if (this.sight.left_article === 10000000) {
|
if (this.sight.left_article === 10000000) {
|
||||||
// Placeholder for new
|
|
||||||
const res = await languageInstance("ru").post("/article", {
|
const res = await languageInstance("ru").post("/article", {
|
||||||
heading: this.sight.ru.left.heading,
|
heading: this.sight.ru.left.heading,
|
||||||
body: this.sight.ru.left.body,
|
body: this.sight.ru.left.body,
|
||||||
@@ -466,7 +452,6 @@ class CreateSightStore {
|
|||||||
this.sight.left_article !== 0 &&
|
this.sight.left_article !== 0 &&
|
||||||
this.sight.left_article !== null
|
this.sight.left_article !== null
|
||||||
) {
|
) {
|
||||||
// Existing, ensure it's up-to-date
|
|
||||||
await languageInstance("ru").patch(
|
await languageInstance("ru").patch(
|
||||||
`/article/${this.sight.left_article}`,
|
`/article/${this.sight.left_article}`,
|
||||||
{ heading: this.sight.ru.left.heading, body: this.sight.ru.left.body }
|
{ heading: this.sight.ru.left.heading, body: this.sight.ru.left.body }
|
||||||
@@ -480,10 +465,7 @@ class CreateSightStore {
|
|||||||
{ heading: this.sight.zh.left.heading, body: this.sight.zh.left.body }
|
{ heading: this.sight.zh.left.heading, body: this.sight.zh.left.body }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// else: left_article is 0, so no left article
|
|
||||||
|
|
||||||
// 2. Right articles are already created in DB and their IDs are in this.sight[lang].right.
|
|
||||||
// We just need to update their content if changed before saving the sight.
|
|
||||||
for (const lang of ["ru", "en", "zh"] as Language[]) {
|
for (const lang of ["ru", "en", "zh"] as Language[]) {
|
||||||
for (const article of this.sight[lang].right) {
|
for (const article of this.sight[lang].right) {
|
||||||
if (article.id == 0 || article.id == null) {
|
if (article.id == 0 || article.id == null) {
|
||||||
@@ -493,14 +475,12 @@ class CreateSightStore {
|
|||||||
heading: article.heading,
|
heading: article.heading,
|
||||||
body: article.body,
|
body: article.body,
|
||||||
});
|
});
|
||||||
// Media for these articles are already linked via createLinkWithRightArticle
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const rightArticleIdsForLink = this.sight[primaryLanguage].right.map(
|
const rightArticleIdsForLink = this.sight[primaryLanguage].right.map(
|
||||||
(a) => a.id
|
(a) => a.id
|
||||||
);
|
);
|
||||||
|
|
||||||
// 3. Create Sight object in DB
|
|
||||||
const sightPayload = {
|
const sightPayload = {
|
||||||
city_id: this.sight.city_id,
|
city_id: this.sight.city_id,
|
||||||
city: this.sight.city,
|
city: this.sight.city,
|
||||||
@@ -520,9 +500,8 @@ class CreateSightStore {
|
|||||||
"/sight",
|
"/sight",
|
||||||
sightPayload
|
sightPayload
|
||||||
);
|
);
|
||||||
const newSightId = response.data.id; // ID of the newly created sight
|
const newSightId = response.data.id;
|
||||||
|
|
||||||
// 4. Update other languages for the sight
|
|
||||||
const otherLanguages = (["ru", "en", "zh"] as Language[]).filter(
|
const otherLanguages = (["ru", "en", "zh"] as Language[]).filter(
|
||||||
(l) => l !== primaryLanguage
|
(l) => l !== primaryLanguage
|
||||||
);
|
);
|
||||||
@@ -543,20 +522,17 @@ class CreateSightStore {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. Link Right Articles to the new Sight
|
|
||||||
for (let i = 0; i < rightArticleIdsForLink.length; i++) {
|
for (let i = 0; i < rightArticleIdsForLink.length; i++) {
|
||||||
await authInstance.post(`/sight/${newSightId}/article`, {
|
await authInstance.post(`/sight/${newSightId}/article`, {
|
||||||
article_id: rightArticleIdsForLink[i],
|
article_id: rightArticleIdsForLink[i],
|
||||||
page_num: i + 1, // Or other logic for page_num
|
page_num: i + 1,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Optionally: this.clearCreateSight(); // To reset form after successful creation
|
|
||||||
this.needLeaveAgree = false;
|
this.needLeaveAgree = false;
|
||||||
return newSightId;
|
return newSightId;
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- Media Upload (Generic, used by dialogs) ---
|
|
||||||
uploadMedia = async (
|
uploadMedia = async (
|
||||||
filename: string,
|
filename: string,
|
||||||
type: number,
|
type: number,
|
||||||
@@ -575,12 +551,12 @@ class CreateSightStore {
|
|||||||
this.fileToUpload = null;
|
this.fileToUpload = null;
|
||||||
this.uploadMediaOpen = false;
|
this.uploadMediaOpen = false;
|
||||||
});
|
});
|
||||||
mediaStore.getMedia(); // Refresh global media list
|
mediaStore.getMedia();
|
||||||
return {
|
return {
|
||||||
id: response.data.id,
|
id: response.data.id,
|
||||||
filename: filename, // Or response.data.filename if backend returns it
|
filename: filename,
|
||||||
media_name: media_name, // Or response.data.media_name
|
media_name: media_name,
|
||||||
media_type: type, // Or response.data.type
|
media_type: type,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error uploading media:", error);
|
console.error("Error uploading media:", error);
|
||||||
@@ -588,15 +564,12 @@ class CreateSightStore {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// For Left Article Media
|
|
||||||
createLinkWithLeftArticle = async (media: MediaItem) => {
|
createLinkWithLeftArticle = async (media: MediaItem) => {
|
||||||
if (!this.sight.left_article || this.sight.left_article === 10000000) {
|
if (!this.sight.left_article || this.sight.left_article === 10000000) {
|
||||||
console.warn(
|
console.warn(
|
||||||
"Left article not selected or is a placeholder. Cannot link media yet."
|
"Left article not selected or is a placeholder. Cannot link media yet."
|
||||||
);
|
);
|
||||||
// If it's a placeholder, we could store the media temporarily and link it after the article is created.
|
|
||||||
// For simplicity, we'll assume the article must exist.
|
|
||||||
// A more robust solution might involve creating the article first if it's a placeholder.
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@@ -655,7 +628,7 @@ class CreateSightStore {
|
|||||||
|
|
||||||
this.sight.ru.right = sortArticles(this.sight.ru.right);
|
this.sight.ru.right = sortArticles(this.sight.ru.right);
|
||||||
this.sight.en.right = sortArticles(this.sight.en.right);
|
this.sight.en.right = sortArticles(this.sight.en.right);
|
||||||
this.sight.zh.right = sortArticles(this.sight.zh.right); // теперь zh тоже сортируется одинаково
|
this.sight.zh.right = sortArticles(this.sight.zh.right);
|
||||||
|
|
||||||
this.needLeaveAgree = true;
|
this.needLeaveAgree = true;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
// @shared/stores/editSightStore.ts
|
|
||||||
import {
|
import {
|
||||||
articlesStore,
|
articlesStore,
|
||||||
authInstance,
|
authInstance,
|
||||||
@@ -96,13 +95,11 @@ class EditSightStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
// Обновляем языковую часть
|
|
||||||
this.sight[language] = {
|
this.sight[language] = {
|
||||||
...this.sight[language],
|
...this.sight[language],
|
||||||
...data,
|
...data,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Только при первом запросе обновляем общую часть
|
|
||||||
if (!this.hasLoadedCommon) {
|
if (!this.hasLoadedCommon) {
|
||||||
this.sight.common = {
|
this.sight.common = {
|
||||||
...this.sight.common,
|
...this.sight.common,
|
||||||
@@ -123,7 +120,6 @@ class EditSightStore {
|
|||||||
let responseEn = await languageInstance("en").get(`/sight/${id}/article`);
|
let responseEn = await languageInstance("en").get(`/sight/${id}/article`);
|
||||||
let responseZh = await languageInstance("zh").get(`/sight/${id}/article`);
|
let responseZh = await languageInstance("zh").get(`/sight/${id}/article`);
|
||||||
|
|
||||||
// Create a map of article IDs to their media
|
|
||||||
const mediaMap = new Map();
|
const mediaMap = new Map();
|
||||||
for (const article of responseRu.data) {
|
for (const article of responseRu.data) {
|
||||||
const responseMedia = await authInstance.get(
|
const responseMedia = await authInstance.get(
|
||||||
@@ -132,7 +128,6 @@ class EditSightStore {
|
|||||||
mediaMap.set(article.id, responseMedia.data);
|
mediaMap.set(article.id, responseMedia.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Function to add media to articles
|
|
||||||
const addMediaToArticles = (articles: any[]) => {
|
const addMediaToArticles = (articles: any[]) => {
|
||||||
return articles.map((article) => ({
|
return articles.map((article) => ({
|
||||||
...article,
|
...article,
|
||||||
@@ -327,28 +322,6 @@ class EditSightStore {
|
|||||||
articles: articleIdsInObject,
|
articles: articleIdsInObject,
|
||||||
});
|
});
|
||||||
|
|
||||||
// await languageInstance("ru").patch(
|
|
||||||
// `/sight/${this.sight.common.left_article}/article`,
|
|
||||||
// {
|
|
||||||
// heading: this.sight.ru.left.heading,
|
|
||||||
// body: this.sight.ru.left.body,
|
|
||||||
// }
|
|
||||||
// );
|
|
||||||
// await languageInstance("en").patch(
|
|
||||||
// `/sight/${this.sight.common.left_article}/article`,
|
|
||||||
// {
|
|
||||||
// heading: this.sight.en.left.heading,
|
|
||||||
// body: this.sight.en.left.body,
|
|
||||||
// }
|
|
||||||
// );
|
|
||||||
// await languageInstance("zh").patch(
|
|
||||||
// `/sight/${this.sight.common.left_article}/article`,
|
|
||||||
// {
|
|
||||||
// heading: this.sight.zh.left.heading,
|
|
||||||
// body: this.sight.zh.left.body,
|
|
||||||
// }
|
|
||||||
// );
|
|
||||||
|
|
||||||
this.needLeaveAgree = false;
|
this.needLeaveAgree = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -400,16 +373,36 @@ class EditSightStore {
|
|||||||
};
|
};
|
||||||
|
|
||||||
createLeftArticle = async () => {
|
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`, {
|
const response = await languageInstance("ru").post(`/article`, {
|
||||||
heading: "",
|
heading: hasAnyName ? ruName : "",
|
||||||
body: "",
|
body: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
this.sight.common.left_article = response.data.id;
|
this.sight.common.left_article = response.data.id;
|
||||||
|
|
||||||
this.sight.ru.left.heading = "";
|
await languageInstance("en").patch(
|
||||||
this.sight.en.left.heading = "";
|
`/article/${this.sight.common.left_article}`,
|
||||||
this.sight.zh.left.heading = "";
|
{
|
||||||
|
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.ru.left.body = "";
|
||||||
this.sight.en.left.body = "";
|
this.sight.en.left.body = "";
|
||||||
this.sight.zh.left.body = "";
|
this.sight.zh.left.body = "";
|
||||||
@@ -569,7 +562,7 @@ class EditSightStore {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
return article_id; // Return the linked article ID
|
return article_id;
|
||||||
};
|
};
|
||||||
|
|
||||||
deleteRightArticleMedia = async (article_id: number, media_id: string) => {
|
deleteRightArticleMedia = async (article_id: number, media_id: string) => {
|
||||||
@@ -675,7 +668,7 @@ class EditSightStore {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
return id; // Return the ID of the newly created article
|
return id;
|
||||||
};
|
};
|
||||||
|
|
||||||
createLinkWithRightArticle = async (
|
createLinkWithRightArticle = async (
|
||||||
@@ -750,7 +743,7 @@ class EditSightStore {
|
|||||||
|
|
||||||
this.sight.ru.right = sortArticles(this.sight.ru.right);
|
this.sight.ru.right = sortArticles(this.sight.ru.right);
|
||||||
this.sight.en.right = sortArticles(this.sight.en.right);
|
this.sight.en.right = sortArticles(this.sight.en.right);
|
||||||
this.sight.zh.right = sortArticles(this.sight.zh.right); // теперь zh тоже сортируется одинаково
|
this.sight.zh.right = sortArticles(this.sight.zh.right);
|
||||||
|
|
||||||
this.needLeaveAgree = true;
|
this.needLeaveAgree = true;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -39,12 +39,11 @@ class MediaStore {
|
|||||||
updateMedia = async (id: string, data: Partial<Media>) => {
|
updateMedia = async (id: string, data: Partial<Media>) => {
|
||||||
const response = await authInstance.patch(`/media/${id}`, data);
|
const response = await authInstance.patch(`/media/${id}`, data);
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
// Update in media array
|
|
||||||
const index = this.media.findIndex((m) => m.id === id);
|
const index = this.media.findIndex((m) => m.id === id);
|
||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
this.media[index] = { ...this.media[index], ...response.data };
|
this.media[index] = { ...this.media[index], ...response.data };
|
||||||
}
|
}
|
||||||
// Update oneMedia if it's the current media being viewed
|
|
||||||
if (this.oneMedia?.id === id) {
|
if (this.oneMedia?.id === id) {
|
||||||
this.oneMedia = { ...this.oneMedia, ...response.data };
|
this.oneMedia = { ...this.oneMedia, ...response.data };
|
||||||
}
|
}
|
||||||
@@ -64,12 +63,11 @@ class MediaStore {
|
|||||||
});
|
});
|
||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
// Update in media array
|
|
||||||
const index = this.media.findIndex((m) => m.id === id);
|
const index = this.media.findIndex((m) => m.id === id);
|
||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
this.media[index] = { ...this.media[index], ...response.data };
|
this.media[index] = { ...this.media[index], ...response.data };
|
||||||
}
|
}
|
||||||
// Update oneMedia if it's the current media being viewed
|
|
||||||
if (this.oneMedia?.id === id) {
|
if (this.oneMedia?.id === id) {
|
||||||
this.oneMedia = { ...this.oneMedia, ...response.data };
|
this.oneMedia = { ...this.oneMedia, ...response.data };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ class ModelLoadingStore {
|
|||||||
makeAutoObservable(this);
|
makeAutoObservable(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Начать отслеживание загрузки модели
|
|
||||||
startLoading(modelId: string) {
|
startLoading(modelId: string) {
|
||||||
this.loadingStates.set(modelId, {
|
this.loadingStates.set(modelId, {
|
||||||
isLoading: true,
|
isLoading: true,
|
||||||
@@ -25,7 +24,6 @@ class ModelLoadingStore {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Обновить прогресс загрузки
|
|
||||||
updateProgress(modelId: string, progress: number) {
|
updateProgress(modelId: string, progress: number) {
|
||||||
const state = this.loadingStates.get(modelId);
|
const state = this.loadingStates.get(modelId);
|
||||||
if (state) {
|
if (state) {
|
||||||
@@ -33,7 +31,6 @@ class ModelLoadingStore {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Завершить загрузку модели
|
|
||||||
finishLoading(modelId: string) {
|
finishLoading(modelId: string) {
|
||||||
const state = this.loadingStates.get(modelId);
|
const state = this.loadingStates.get(modelId);
|
||||||
if (state) {
|
if (state) {
|
||||||
@@ -42,12 +39,10 @@ class ModelLoadingStore {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Остановить загрузку (в случае ошибки)
|
|
||||||
stopLoading(modelId: string) {
|
stopLoading(modelId: string) {
|
||||||
this.loadingStates.delete(modelId);
|
this.loadingStates.delete(modelId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Обработать ошибку загрузки
|
|
||||||
handleError(modelId: string, error?: string) {
|
handleError(modelId: string, error?: string) {
|
||||||
const state = this.loadingStates.get(modelId);
|
const state = this.loadingStates.get(modelId);
|
||||||
if (state) {
|
if (state) {
|
||||||
@@ -56,26 +51,22 @@ class ModelLoadingStore {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Получить состояние загрузки для конкретной модели
|
|
||||||
getLoadingState(modelId: string): ModelLoadingState | undefined {
|
getLoadingState(modelId: string): ModelLoadingState | undefined {
|
||||||
return this.loadingStates.get(modelId);
|
return this.loadingStates.get(modelId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Проверить, загружается ли какая-либо модель
|
|
||||||
get isAnyModelLoading(): boolean {
|
get isAnyModelLoading(): boolean {
|
||||||
return Array.from(this.loadingStates.values()).some(
|
return Array.from(this.loadingStates.values()).some(
|
||||||
(state) => state.isLoading
|
(state) => state.isLoading
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Получить все загружающиеся модели
|
|
||||||
get loadingModels(): ModelLoadingState[] {
|
get loadingModels(): ModelLoadingState[] {
|
||||||
return Array.from(this.loadingStates.values()).filter(
|
return Array.from(this.loadingStates.values()).filter(
|
||||||
(state) => state.isLoading
|
(state) => state.isLoading
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Получить общий прогресс всех загружающихся моделей
|
|
||||||
get overallProgress(): number {
|
get overallProgress(): number {
|
||||||
const loadingModels = this.loadingModels;
|
const loadingModels = this.loadingModels;
|
||||||
if (loadingModels.length === 0) return 100;
|
if (loadingModels.length === 0) return 100;
|
||||||
@@ -87,12 +78,10 @@ class ModelLoadingStore {
|
|||||||
return Math.round(totalProgress / loadingModels.length);
|
return Math.round(totalProgress / loadingModels.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Проверить, заблокировано ли сохранение (есть ли загружающиеся модели)
|
|
||||||
get isSaveBlocked(): boolean {
|
get isSaveBlocked(): boolean {
|
||||||
return this.isAnyModelLoading;
|
return this.isAnyModelLoading;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Очистить все состояния загрузки
|
|
||||||
clearAll() {
|
clearAll() {
|
||||||
this.loadingStates.clear();
|
this.loadingStates.clear();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { makeAutoObservable, runInAction } from "mobx";
|
|||||||
import { authInstance } from "@shared";
|
import { authInstance } from "@shared";
|
||||||
|
|
||||||
export type Route = {
|
export type Route = {
|
||||||
|
route_name: string;
|
||||||
carrier: string;
|
carrier: string;
|
||||||
carrier_id: number;
|
carrier_id: number;
|
||||||
center_latitude: number;
|
center_latitude: number;
|
||||||
@@ -97,6 +98,7 @@ class RouteStore {
|
|||||||
};
|
};
|
||||||
|
|
||||||
editRouteData = {
|
editRouteData = {
|
||||||
|
route_name: "",
|
||||||
carrier: "",
|
carrier: "",
|
||||||
carrier_id: 0,
|
carrier_id: 0,
|
||||||
center_latitude: "",
|
center_latitude: "",
|
||||||
@@ -110,7 +112,7 @@ class RouteStore {
|
|||||||
route_sys_number: "",
|
route_sys_number: "",
|
||||||
scale_max: 0,
|
scale_max: 0,
|
||||||
scale_min: 0,
|
scale_min: 0,
|
||||||
video_preview: "",
|
video_preview: "" as string | undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
setEditRouteData = (data: any) => {
|
setEditRouteData = (data: any) => {
|
||||||
@@ -118,6 +120,9 @@ class RouteStore {
|
|||||||
};
|
};
|
||||||
|
|
||||||
editRoute = async (id: number) => {
|
editRoute = async (id: number) => {
|
||||||
|
if (!this.editRouteData.video_preview) {
|
||||||
|
delete this.editRouteData.video_preview;
|
||||||
|
}
|
||||||
const response = await authInstance.patch(`/route/${id}`, {
|
const response = await authInstance.patch(`/route/${id}`, {
|
||||||
...this.editRouteData,
|
...this.editRouteData,
|
||||||
center_latitude: parseFloat(this.editRouteData.center_latitude),
|
center_latitude: parseFloat(this.editRouteData.center_latitude),
|
||||||
|
|||||||
@@ -58,41 +58,6 @@ class SightsStore {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// getSight = async (id: number) => {
|
|
||||||
// const response = await authInstance.get(`/sight/${id}`);
|
|
||||||
|
|
||||||
// runInAction(() => {
|
|
||||||
// this.sight = response.data;
|
|
||||||
// editSightStore.sightInfo = {
|
|
||||||
// ...editSightStore.sightInfo,
|
|
||||||
// id: response.data.id,
|
|
||||||
// city_id: response.data.city_id,
|
|
||||||
// city: response.data.city,
|
|
||||||
// latitude: response.data.latitude,
|
|
||||||
// longitude: response.data.longitude,
|
|
||||||
// thumbnail: response.data.thumbnail,
|
|
||||||
// watermark_lu: response.data.watermark_lu,
|
|
||||||
// watermark_rd: response.data.watermark_rd,
|
|
||||||
// left_article: response.data.left_article,
|
|
||||||
// preview_media: response.data.preview_media,
|
|
||||||
// video_preview: response.data.video_preview,
|
|
||||||
|
|
||||||
// [languageStore.language]: {
|
|
||||||
// info: {
|
|
||||||
// name: response.data.name,
|
|
||||||
// address: response.data.address,
|
|
||||||
// },
|
|
||||||
// left: {
|
|
||||||
// heading: articlesStore.articles[languageStore.language].find(
|
|
||||||
// (article) => article.id === response.data.left_article
|
|
||||||
// )?.heading,
|
|
||||||
// body: articlesStore.articles[languageStore.language].find(
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// };
|
|
||||||
// });
|
|
||||||
// };
|
|
||||||
|
|
||||||
createSightAction = async (
|
createSightAction = async (
|
||||||
city: number,
|
city: number,
|
||||||
coordinates: { latitude: number; longitude: number }
|
coordinates: { latitude: number; longitude: number }
|
||||||
@@ -167,16 +132,12 @@ class SightsStore {
|
|||||||
common: boolean
|
common: boolean
|
||||||
) => {
|
) => {
|
||||||
if (common) {
|
if (common) {
|
||||||
// @ts-ignore
|
|
||||||
this.sight!.common = {
|
this.sight!.common = {
|
||||||
// @ts-ignore
|
|
||||||
...this.sight!.common,
|
...this.sight!.common,
|
||||||
...content,
|
...content,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
// @ts-ignore
|
|
||||||
this.sight![language] = {
|
this.sight![language] = {
|
||||||
// @ts-ignore
|
|
||||||
...this.sight![language],
|
...this.sight![language],
|
||||||
...content,
|
...content,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import { authInstance } from "@shared";
|
import { authInstance } from "@shared";
|
||||||
|
|
||||||
import { makeAutoObservable, runInAction } from "mobx";
|
import { makeAutoObservable, runInAction } from "mobx";
|
||||||
// Импорт функции сброса кешей карты
|
|
||||||
// import { clearMapCaches } from "../../pages/MapPage";
|
|
||||||
import {
|
import {
|
||||||
articlesStore,
|
articlesStore,
|
||||||
cityStore,
|
cityStore,
|
||||||
@@ -35,9 +33,7 @@ class SnapshotStore {
|
|||||||
makeAutoObservable(this);
|
makeAutoObservable(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Функция для сброса всех кешей в приложении
|
|
||||||
private clearAllCaches = () => {
|
private clearAllCaches = () => {
|
||||||
// Сброс кешей статей
|
|
||||||
articlesStore.articleList = {
|
articlesStore.articleList = {
|
||||||
ru: { data: [], loaded: false },
|
ru: { data: [], loaded: false },
|
||||||
en: { data: [], loaded: false },
|
en: { data: [], loaded: false },
|
||||||
@@ -47,7 +43,6 @@ class SnapshotStore {
|
|||||||
articlesStore.articleData = null;
|
articlesStore.articleData = null;
|
||||||
articlesStore.articleMedia = null;
|
articlesStore.articleMedia = null;
|
||||||
|
|
||||||
// Сброс кешей городов
|
|
||||||
cityStore.cities = {
|
cityStore.cities = {
|
||||||
ru: { data: [], loaded: false },
|
ru: { data: [], loaded: false },
|
||||||
en: { data: [], loaded: false },
|
en: { data: [], loaded: false },
|
||||||
@@ -56,21 +51,18 @@ class SnapshotStore {
|
|||||||
cityStore.ruCities = { data: [], loaded: false };
|
cityStore.ruCities = { data: [], loaded: false };
|
||||||
cityStore.city = {};
|
cityStore.city = {};
|
||||||
|
|
||||||
// Сброс кешей стран
|
|
||||||
countryStore.countries = {
|
countryStore.countries = {
|
||||||
ru: { data: [], loaded: false },
|
ru: { data: [], loaded: false },
|
||||||
en: { data: [], loaded: false },
|
en: { data: [], loaded: false },
|
||||||
zh: { data: [], loaded: false },
|
zh: { data: [], loaded: false },
|
||||||
};
|
};
|
||||||
|
|
||||||
// Сброс кешей перевозчиков
|
|
||||||
carrierStore.carriers = {
|
carrierStore.carriers = {
|
||||||
ru: { data: [], loaded: false },
|
ru: { data: [], loaded: false },
|
||||||
en: { data: [], loaded: false },
|
en: { data: [], loaded: false },
|
||||||
zh: { data: [], loaded: false },
|
zh: { data: [], loaded: false },
|
||||||
};
|
};
|
||||||
|
|
||||||
// Сброс кешей станций
|
|
||||||
stationsStore.stationLists = {
|
stationsStore.stationLists = {
|
||||||
ru: { data: [], loaded: false },
|
ru: { data: [], loaded: false },
|
||||||
en: { data: [], loaded: false },
|
en: { data: [], loaded: false },
|
||||||
@@ -78,24 +70,18 @@ class SnapshotStore {
|
|||||||
};
|
};
|
||||||
stationsStore.stationPreview = {};
|
stationsStore.stationPreview = {};
|
||||||
|
|
||||||
// Сброс кешей достопримечательностей
|
|
||||||
sightsStore.sights = [];
|
sightsStore.sights = [];
|
||||||
sightsStore.sight = null;
|
sightsStore.sight = null;
|
||||||
|
|
||||||
// Сброс кешей маршрутов
|
|
||||||
routeStore.routes = { data: [], loaded: false };
|
routeStore.routes = { data: [], loaded: false };
|
||||||
|
|
||||||
// Сброс кешей транспорта
|
|
||||||
vehicleStore.vehicles = { data: [], loaded: false };
|
vehicleStore.vehicles = { data: [], loaded: false };
|
||||||
|
|
||||||
// Сброс кешей пользователей
|
|
||||||
userStore.users = { data: [], loaded: false };
|
userStore.users = { data: [], loaded: false };
|
||||||
|
|
||||||
// Сброс кешей медиа
|
|
||||||
mediaStore.media = [];
|
mediaStore.media = [];
|
||||||
mediaStore.oneMedia = null;
|
mediaStore.oneMedia = null;
|
||||||
|
|
||||||
// Сброс кешей создания и редактирования достопримечательностей
|
|
||||||
createSightStore.sight = JSON.parse(
|
createSightStore.sight = JSON.parse(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
city_id: 0,
|
city_id: 0,
|
||||||
@@ -173,26 +159,21 @@ class SnapshotStore {
|
|||||||
editSightStore.fileToUpload = null;
|
editSightStore.fileToUpload = null;
|
||||||
editSightStore.needLeaveAgree = false;
|
editSightStore.needLeaveAgree = false;
|
||||||
|
|
||||||
// Сброс кешей устройств
|
|
||||||
devicesStore.devices = [];
|
devicesStore.devices = [];
|
||||||
devicesStore.uuid = null;
|
devicesStore.uuid = null;
|
||||||
devicesStore.sendSnapshotModalOpen = false;
|
devicesStore.sendSnapshotModalOpen = false;
|
||||||
|
|
||||||
// Сброс кешей авторизации (кроме токена)
|
|
||||||
authStore.payload = null;
|
authStore.payload = null;
|
||||||
authStore.error = null;
|
authStore.error = null;
|
||||||
authStore.isLoading = false;
|
authStore.isLoading = false;
|
||||||
|
|
||||||
// Сброс кешей карты (если они загружены)
|
|
||||||
try {
|
try {
|
||||||
// Сбрасываем кеши mapStore если он доступен
|
|
||||||
if (typeof window !== "undefined" && (window as any).mapStore) {
|
if (typeof window !== "undefined" && (window as any).mapStore) {
|
||||||
(window as any).mapStore.routes = [];
|
(window as any).mapStore.routes = [];
|
||||||
(window as any).mapStore.stations = [];
|
(window as any).mapStore.stations = [];
|
||||||
(window as any).mapStore.sights = [];
|
(window as any).mapStore.sights = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Сбрасываем кеши MapService если он доступен
|
|
||||||
if (typeof window !== "undefined" && (window as any).mapServiceInstance) {
|
if (typeof window !== "undefined" && (window as any).mapServiceInstance) {
|
||||||
(window as any).mapServiceInstance.clearCaches();
|
(window as any).mapServiceInstance.clearCaches();
|
||||||
}
|
}
|
||||||
@@ -200,7 +181,6 @@ class SnapshotStore {
|
|||||||
console.warn("Не удалось сбросить кеши карты:", error);
|
console.warn("Не удалось сбросить кеши карты:", error);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Сброс localStorage кешей (кроме токена авторизации)
|
|
||||||
const token = localStorage.getItem("token");
|
const token = localStorage.getItem("token");
|
||||||
const rememberedEmail = localStorage.getItem("rememberedEmail");
|
const rememberedEmail = localStorage.getItem("rememberedEmail");
|
||||||
const rememberedPassword = localStorage.getItem("rememberedPassword");
|
const rememberedPassword = localStorage.getItem("rememberedPassword");
|
||||||
@@ -208,14 +188,12 @@ class SnapshotStore {
|
|||||||
localStorage.clear();
|
localStorage.clear();
|
||||||
sessionStorage.clear();
|
sessionStorage.clear();
|
||||||
|
|
||||||
// Восстанавливаем важные данные
|
|
||||||
if (token) localStorage.setItem("token", token);
|
if (token) localStorage.setItem("token", token);
|
||||||
if (rememberedEmail)
|
if (rememberedEmail)
|
||||||
localStorage.setItem("rememberedEmail", rememberedEmail);
|
localStorage.setItem("rememberedEmail", rememberedEmail);
|
||||||
if (rememberedPassword)
|
if (rememberedPassword)
|
||||||
localStorage.setItem("rememberedPassword", rememberedPassword);
|
localStorage.setItem("rememberedPassword", rememberedPassword);
|
||||||
|
|
||||||
// Сброс кешей карты (если они есть)
|
|
||||||
const mapPositionKey = "mapPosition";
|
const mapPositionKey = "mapPosition";
|
||||||
const activeSectionKey = "mapActiveSection";
|
const activeSectionKey = "mapActiveSection";
|
||||||
if (localStorage.getItem(mapPositionKey)) {
|
if (localStorage.getItem(mapPositionKey)) {
|
||||||
@@ -225,7 +203,6 @@ class SnapshotStore {
|
|||||||
localStorage.removeItem(activeSectionKey);
|
localStorage.removeItem(activeSectionKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Попытка очистить кеш браузера (если поддерживается)
|
|
||||||
if ("caches" in window) {
|
if ("caches" in window) {
|
||||||
try {
|
try {
|
||||||
caches.keys().then((cacheNames) => {
|
caches.keys().then((cacheNames) => {
|
||||||
@@ -240,7 +217,6 @@ class SnapshotStore {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Попытка очистить IndexedDB (если поддерживается)
|
|
||||||
if ("indexedDB" in window) {
|
if ("indexedDB" in window) {
|
||||||
try {
|
try {
|
||||||
indexedDB.databases().then((databases) => {
|
indexedDB.databases().then((databases) => {
|
||||||
@@ -284,10 +260,8 @@ class SnapshotStore {
|
|||||||
};
|
};
|
||||||
|
|
||||||
restoreSnapshot = async (id: string) => {
|
restoreSnapshot = async (id: string) => {
|
||||||
// Сначала сбрасываем все кеши
|
|
||||||
this.clearAllCaches();
|
this.clearAllCaches();
|
||||||
|
|
||||||
// Затем восстанавливаем снапшот
|
|
||||||
await authInstance.post(`/snapshots/${id}/restore`);
|
await authInstance.post(`/snapshots/${id}/restore`);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ type StationLanguageData = {
|
|||||||
name: string;
|
name: string;
|
||||||
system_name: string;
|
system_name: string;
|
||||||
address: string;
|
address: string;
|
||||||
loaded: boolean; // Indicates if this language's data has been loaded/modified
|
loaded: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
type StationCommonData = {
|
type StationCommonData = {
|
||||||
@@ -92,7 +92,6 @@ class StationsStore {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// This will store the full station data, keyed by ID and then by language
|
|
||||||
stationPreview: Record<
|
stationPreview: Record<
|
||||||
string,
|
string,
|
||||||
Record<string, { loaded: boolean; data: Station }>
|
Record<string, { loaded: boolean; data: Station }>
|
||||||
@@ -264,7 +263,6 @@ class StationsStore {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// Sets language-specific station data
|
|
||||||
setLanguageEditStationData = (
|
setLanguageEditStationData = (
|
||||||
language: Language,
|
language: Language,
|
||||||
data: Partial<StationLanguageData>
|
data: Partial<StationLanguageData>
|
||||||
@@ -295,7 +293,7 @@ class StationsStore {
|
|||||||
`/station/${id}`,
|
`/station/${id}`,
|
||||||
{
|
{
|
||||||
name: name || "",
|
name: name || "",
|
||||||
system_name: name || "", // system_name is often derived from name
|
system_name: name || "",
|
||||||
description: description || "",
|
description: description || "",
|
||||||
address: address || "",
|
address: address || "",
|
||||||
...commonDataPayload,
|
...commonDataPayload,
|
||||||
@@ -303,7 +301,6 @@ class StationsStore {
|
|||||||
);
|
);
|
||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
// Update the cached preview data and station lists after successful patch
|
|
||||||
if (this.stationPreview[id]) {
|
if (this.stationPreview[id]) {
|
||||||
this.stationPreview[id][language] = {
|
this.stationPreview[id][language] = {
|
||||||
loaded: true,
|
loaded: true,
|
||||||
@@ -343,11 +340,11 @@ class StationsStore {
|
|||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.stations = this.stations.filter((station) => station.id !== id);
|
this.stations = this.stations.filter((station) => station.id !== id);
|
||||||
// Also clear from stationPreview cache
|
|
||||||
if (this.stationPreview[id]) {
|
if (this.stationPreview[id]) {
|
||||||
delete this.stationPreview[id];
|
delete this.stationPreview[id];
|
||||||
}
|
}
|
||||||
// Clear from stationLists as well for all languages
|
|
||||||
for (const lang of ["ru", "en", "zh"] as const) {
|
for (const lang of ["ru", "en", "zh"] as const) {
|
||||||
if (this.stationLists[lang].data) {
|
if (this.stationLists[lang].data) {
|
||||||
this.stationLists[lang].data = this.stationLists[lang].data.filter(
|
this.stationLists[lang].data = this.stationLists[lang].data.filter(
|
||||||
@@ -421,12 +418,11 @@ class StationsStore {
|
|||||||
delete commonDataPayload.icon;
|
delete commonDataPayload.icon;
|
||||||
}
|
}
|
||||||
|
|
||||||
// First create station in Russian
|
|
||||||
const { name, address } = this.createStationData[language];
|
const { name, address } = this.createStationData[language];
|
||||||
const description = this.createStationData.common.description;
|
const description = this.createStationData.common.description;
|
||||||
const response = await languageInstance(language).post("/station", {
|
const response = await languageInstance(language).post("/station", {
|
||||||
name: name || "",
|
name: name || "",
|
||||||
system_name: name || "", // system_name is often derived from name
|
system_name: name || "",
|
||||||
description: description || "",
|
description: description || "",
|
||||||
address: address || "",
|
address: address || "",
|
||||||
...commonDataPayload,
|
...commonDataPayload,
|
||||||
@@ -438,7 +434,6 @@ class StationsStore {
|
|||||||
|
|
||||||
const stationId = response.data.id;
|
const stationId = response.data.id;
|
||||||
|
|
||||||
// Then update for other languages
|
|
||||||
for (const lang of ["ru", "en", "zh"].filter(
|
for (const lang of ["ru", "en", "zh"].filter(
|
||||||
(lang) => lang !== language
|
(lang) => lang !== language
|
||||||
) as Language[]) {
|
) as Language[]) {
|
||||||
@@ -448,7 +443,7 @@ class StationsStore {
|
|||||||
`/station/${stationId}`,
|
`/station/${stationId}`,
|
||||||
{
|
{
|
||||||
name: name || "",
|
name: name || "",
|
||||||
system_name: name || "", // system_name is often derived from name
|
system_name: name || "",
|
||||||
description: description || "",
|
description: description || "",
|
||||||
address: address || "",
|
address: address || "",
|
||||||
...commonDataPayload,
|
...commonDataPayload,
|
||||||
@@ -507,7 +502,6 @@ class StationsStore {
|
|||||||
return response.data;
|
return response.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Reset editStationData when navigating away or after saving
|
|
||||||
resetEditStationData = () => {
|
resetEditStationData = () => {
|
||||||
this.editStationData = {
|
this.editStationData = {
|
||||||
ru: {
|
ru: {
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ import {
|
|||||||
devicesStore,
|
devicesStore,
|
||||||
Modal,
|
Modal,
|
||||||
snapshotStore,
|
snapshotStore,
|
||||||
vehicleStore, // Not directly used in this component's rendering logic anymore
|
vehicleStore,
|
||||||
} from "@shared"; // Assuming @shared exports these
|
} from "@shared";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { Button, Checkbox, Typography } from "@mui/material";
|
import { Button, Checkbox, Typography } from "@mui/material";
|
||||||
@@ -23,12 +23,10 @@ import { useNavigate } from "react-router-dom";
|
|||||||
export type ConnectedDevice = string;
|
export type ConnectedDevice = string;
|
||||||
|
|
||||||
interface Snapshot {
|
interface Snapshot {
|
||||||
ID: string; // Assuming ID is string based on usage
|
ID: string;
|
||||||
Name: string;
|
Name: string;
|
||||||
// Add other snapshot properties if needed
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- HELPER FUNCTIONS ---
|
|
||||||
const formatDate = (dateString: string | null) => {
|
const formatDate = (dateString: string | null) => {
|
||||||
if (!dateString) return "Нет данных";
|
if (!dateString) return "Нет данных";
|
||||||
try {
|
try {
|
||||||
@@ -76,12 +74,7 @@ function createData(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// This function transforms the raw device data (which includes vehicle and device_status)
|
const transformDevicesToRows = (vehicles: Vehicle[]): TableRowData[] => {
|
||||||
// into the format expected by the table. It now filters for devices that have a UUID.
|
|
||||||
const transformDevicesToRows = (
|
|
||||||
vehicles: Vehicle[]
|
|
||||||
// devices: ConnectedDevice[]
|
|
||||||
): TableRowData[] => {
|
|
||||||
return vehicles.map((vehicle) => {
|
return vehicles.map((vehicle) => {
|
||||||
const uuid = vehicle.vehicle.uuid;
|
const uuid = vehicle.vehicle.uuid;
|
||||||
if (!uuid)
|
if (!uuid)
|
||||||
@@ -115,26 +108,21 @@ export const DevicesTable = observer(() => {
|
|||||||
} = devicesStore;
|
} = devicesStore;
|
||||||
|
|
||||||
const { snapshots, getSnapshots } = snapshotStore;
|
const { snapshots, getSnapshots } = snapshotStore;
|
||||||
const { getVehicles, vehicles } = vehicleStore; // Removed as devicesStore.devices should be the source of truth
|
const { getVehicles, vehicles } = vehicleStore;
|
||||||
const { devices } = devicesStore;
|
const { devices } = devicesStore;
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [selectedDeviceUuids, setSelectedDeviceUuids] = useState<string[]>([]);
|
const [selectedDeviceUuids, setSelectedDeviceUuids] = useState<string[]>([]);
|
||||||
|
|
||||||
// Transform the raw devices data into rows suitable for the table
|
const currentTableRows = transformDevicesToRows(vehicles.data as Vehicle[]);
|
||||||
// This will also filter out devices without a UUID, as those cannot be acted upon.
|
|
||||||
const currentTableRows = transformDevicesToRows(
|
|
||||||
vehicles.data as Vehicle[]
|
|
||||||
// devices as ConnectedDevice[]
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
await getVehicles(); // Not strictly needed if devicesStore.devices is populated correctly by getDevices
|
await getVehicles();
|
||||||
await getDevices(); // This should fetch the combined vehicle/device_status data
|
await getDevices();
|
||||||
await getSnapshots();
|
await getSnapshots();
|
||||||
};
|
};
|
||||||
fetchData();
|
fetchData();
|
||||||
}, [getDevices, getSnapshots]); // Added dependencies
|
}, [getDevices, getSnapshots]);
|
||||||
|
|
||||||
const isAllSelected =
|
const isAllSelected =
|
||||||
currentTableRows.length > 0 &&
|
currentTableRows.length > 0 &&
|
||||||
@@ -144,7 +132,6 @@ export const DevicesTable = observer(() => {
|
|||||||
if (isAllSelected) {
|
if (isAllSelected) {
|
||||||
setSelectedDeviceUuids([]);
|
setSelectedDeviceUuids([]);
|
||||||
} else {
|
} else {
|
||||||
// Select all device UUIDs from the *currently visible and selectable* rows
|
|
||||||
setSelectedDeviceUuids(
|
setSelectedDeviceUuids(
|
||||||
currentTableRows.map((row) => row.device_uuid ?? "")
|
currentTableRows.map((row) => row.device_uuid ?? "")
|
||||||
);
|
);
|
||||||
@@ -171,14 +158,13 @@ export const DevicesTable = observer(() => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleReloadStatus = async (uuid: string) => {
|
const handleReloadStatus = async (uuid: string) => {
|
||||||
setSelectedDevice(uuid); // Sets the device in the store, useful for context elsewhere
|
setSelectedDevice(uuid);
|
||||||
try {
|
try {
|
||||||
await authInstance.post(`/devices/${uuid}/request-status`);
|
await authInstance.post(`/devices/${uuid}/request-status`);
|
||||||
await getVehicles();
|
await getVehicles();
|
||||||
await getDevices(); // Refresh devices to show updated status
|
await getDevices();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error requesting status for device ${uuid}:`, error);
|
console.error(`Error requesting status for device ${uuid}:`, error);
|
||||||
// Optionally: show a user-facing error message
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -200,22 +186,16 @@ export const DevicesTable = observer(() => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
// Create an array of promises for all snapshot requests
|
|
||||||
const snapshotPromises = selectedDeviceUuids.map((deviceUuid) => {
|
const snapshotPromises = selectedDeviceUuids.map((deviceUuid) => {
|
||||||
return send(deviceUuid);
|
return send(deviceUuid);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Wait for all promises to settle (either resolve or reject)
|
|
||||||
await Promise.allSettled(snapshotPromises);
|
await Promise.allSettled(snapshotPromises);
|
||||||
|
|
||||||
// After all requests are attempted
|
await getDevices();
|
||||||
await getDevices(); // Refresh the device list
|
setSelectedDeviceUuids([]);
|
||||||
setSelectedDeviceUuids([]); // Clear the selection
|
toggleSendSnapshotModal();
|
||||||
toggleSendSnapshotModal(); // Close the modal
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// This catch block might not be hit if Promise.allSettled is used,
|
|
||||||
// as it doesn't reject on individual promise failures.
|
|
||||||
// Individual errors should be handled if needed within the .map or by checking results.
|
|
||||||
console.error("Error in snapshot sending process:", error);
|
console.error("Error in snapshot sending process:", error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -235,7 +215,7 @@ export const DevicesTable = observer(() => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex justify-end p-3 gap-2">
|
<div className="flex justify-end p-3 gap-2">
|
||||||
<Button
|
<Button
|
||||||
variant="outlined" // Changed to outlined for distinction
|
variant="outlined"
|
||||||
onClick={handleSelectAllDevices}
|
onClick={handleSelectAllDevices}
|
||||||
size="small"
|
size="small"
|
||||||
>
|
>
|
||||||
@@ -286,7 +266,6 @@ export const DevicesTable = observer(() => {
|
|||||||
)}
|
)}
|
||||||
selected={selectedDeviceUuids.includes(row.device_uuid ?? "")}
|
selected={selectedDeviceUuids.includes(row.device_uuid ?? "")}
|
||||||
onClick={(event) => {
|
onClick={(event) => {
|
||||||
// Allow clicking row to toggle checkbox, if not clicking on button
|
|
||||||
if (
|
if (
|
||||||
(event.target as HTMLElement).closest("button") === null &&
|
(event.target as HTMLElement).closest("button") === null &&
|
||||||
(event.target as HTMLElement).closest(
|
(event.target as HTMLElement).closest(
|
||||||
@@ -308,7 +287,6 @@ export const DevicesTable = observer(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only toggle checkbox if Shift key is not pressed
|
|
||||||
if (!event.shiftKey) {
|
if (!event.shiftKey) {
|
||||||
handleSelectDevice(
|
handleSelectDevice(
|
||||||
{
|
{
|
||||||
@@ -317,7 +295,7 @@ export const DevicesTable = observer(() => {
|
|||||||
row.device_uuid ?? ""
|
row.device_uuid ?? ""
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
} as React.ChangeEvent<HTMLInputElement>, // Simulate event
|
} as React.ChangeEvent<HTMLInputElement>,
|
||||||
row.device_uuid ?? ""
|
row.device_uuid ?? ""
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -445,7 +423,7 @@ export const DevicesTable = observer(() => {
|
|||||||
</strong>
|
</strong>
|
||||||
</Typography>
|
</Typography>
|
||||||
<div className="mt-2 flex flex-col gap-2 max-h-[300px] overflow-y-auto pr-2">
|
<div className="mt-2 flex flex-col gap-2 max-h-[300px] overflow-y-auto pr-2">
|
||||||
{snapshots && (snapshots as Snapshot[]).length > 0 ? ( // Cast snapshots
|
{snapshots && (snapshots as Snapshot[]).length > 0 ? (
|
||||||
(snapshots as Snapshot[]).map((snapshot) => (
|
(snapshots as Snapshot[]).map((snapshot) => (
|
||||||
<Button
|
<Button
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useRef, useState, DragEvent, useEffect } from "react";
|
import React, { useRef, DragEvent } from "react";
|
||||||
import { Paper, Box, Typography, Button, Tooltip } from "@mui/material";
|
import { Paper, Box, Typography, Button, Tooltip } from "@mui/material";
|
||||||
import { X, Info, Plus } from "lucide-react"; // Assuming lucide-react for icons
|
import { X, Info, Plus } from "lucide-react";
|
||||||
import { editSightStore } from "@shared";
|
import { editSightStore } from "@shared";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
interface ImageUploadCardProps {
|
interface ImageUploadCardProps {
|
||||||
@@ -27,18 +27,9 @@ export const ImageUploadCard: React.FC<ImageUploadCardProps> = ({
|
|||||||
tooltipText,
|
tooltipText,
|
||||||
}) => {
|
}) => {
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
const [isDragOver, setIsDragOver] = useState(false);
|
|
||||||
const { setFileToUpload } = editSightStore;
|
const { setFileToUpload } = editSightStore;
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (isDragOver) {
|
|
||||||
console.log("isDragOver");
|
|
||||||
}
|
|
||||||
}, [isDragOver]);
|
|
||||||
|
|
||||||
// --- Click to select file ---
|
|
||||||
const handleZoneClick = () => {
|
const handleZoneClick = () => {
|
||||||
// Trigger the hidden file input click
|
|
||||||
fileInputRef.current?.click();
|
fileInputRef.current?.click();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -59,28 +50,25 @@ export const ImageUploadCard: React.FC<ImageUploadCardProps> = ({
|
|||||||
toast.error("Пожалуйста, выберите изображение");
|
toast.error("Пожалуйста, выберите изображение");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Reset the input value so selecting the same file again triggers change
|
|
||||||
event.target.value = "";
|
event.target.value = "";
|
||||||
};
|
};
|
||||||
|
|
||||||
const token = localStorage.getItem("token");
|
const token = localStorage.getItem("token");
|
||||||
// --- Drag and Drop Handlers ---
|
|
||||||
const handleDragOver = (event: DragEvent<HTMLDivElement>) => {
|
const handleDragOver = (event: DragEvent<HTMLDivElement>) => {
|
||||||
event.preventDefault(); // Crucial to allow a drop
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
setIsDragOver(true);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDragLeave = (event: DragEvent<HTMLDivElement>) => {
|
const handleDragLeave = (event: DragEvent<HTMLDivElement>) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
setIsDragOver(false);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDrop = async (event: DragEvent<HTMLDivElement>) => {
|
const handleDrop = async (event: DragEvent<HTMLDivElement>) => {
|
||||||
event.preventDefault(); // Crucial to allow a drop
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
setIsDragOver(false);
|
|
||||||
|
|
||||||
const files = event.dataTransfer.files;
|
const files = event.dataTransfer.files;
|
||||||
if (files && files.length > 0) {
|
if (files && files.length > 0) {
|
||||||
@@ -132,7 +120,6 @@ export const ImageUploadCard: React.FC<ImageUploadCardProps> = ({
|
|||||||
cursor: imageUrl ? "pointer" : "default",
|
cursor: imageUrl ? "pointer" : "default",
|
||||||
}}
|
}}
|
||||||
onClick={onImageClick}
|
onClick={onImageClick}
|
||||||
// Removed onClick on the main Box to avoid conflicts
|
|
||||||
>
|
>
|
||||||
{imageUrl && (
|
{imageUrl && (
|
||||||
<button
|
<button
|
||||||
@@ -165,7 +152,7 @@ export const ImageUploadCard: React.FC<ImageUploadCardProps> = ({
|
|||||||
borderRadius: 1,
|
borderRadius: 1,
|
||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
}}
|
}}
|
||||||
onClick={handleZoneClick} // Click handler for the zone
|
onClick={handleZoneClick}
|
||||||
onDragOver={handleDragOver}
|
onDragOver={handleDragOver}
|
||||||
onDragLeave={handleDragLeave}
|
onDragLeave={handleDragLeave}
|
||||||
onDrop={handleDrop}
|
onDrop={handleDrop}
|
||||||
@@ -179,8 +166,8 @@ export const ImageUploadCard: React.FC<ImageUploadCardProps> = ({
|
|||||||
color="primary"
|
color="primary"
|
||||||
startIcon={<Plus color="white" size={18} />}
|
startIcon={<Plus color="white" size={18} />}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation(); // Prevent `handleZoneClick` from firing
|
e.stopPropagation();
|
||||||
onSelectFileClick(); // This button might trigger a different modal
|
onSelectFileClick();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Выбрать файл
|
Выбрать файл
|
||||||
@@ -191,7 +178,7 @@ export const ImageUploadCard: React.FC<ImageUploadCardProps> = ({
|
|||||||
ref={fileInputRef}
|
ref={fileInputRef}
|
||||||
onChange={handleFileInputChange}
|
onChange={handleFileInputChange}
|
||||||
style={{ display: "none" }}
|
style={{ display: "none" }}
|
||||||
accept="image/*" // Accept only image files
|
accept="image/*"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -48,21 +48,24 @@ export const Layout: React.FC<LayoutProps> = observer(({ children }) => {
|
|||||||
<Box sx={{ display: "flex" }}>
|
<Box sx={{ display: "flex" }}>
|
||||||
<AppBar position="fixed" open={open}>
|
<AppBar position="fixed" open={open}>
|
||||||
<Toolbar className="flex justify-between">
|
<Toolbar className="flex justify-between">
|
||||||
<IconButton
|
<div className="flex items-center">
|
||||||
color="inherit"
|
<IconButton
|
||||||
aria-label="open drawer"
|
color="inherit"
|
||||||
onClick={handleDrawerOpen}
|
aria-label="open drawer"
|
||||||
edge="start"
|
onClick={handleDrawerOpen}
|
||||||
sx={[
|
edge="start"
|
||||||
{
|
sx={[
|
||||||
marginRight: 5,
|
{
|
||||||
},
|
marginRight: 5,
|
||||||
open && { display: "none" },
|
},
|
||||||
]}
|
open && { display: "none" },
|
||||||
>
|
]}
|
||||||
<Menu />
|
>
|
||||||
</IconButton>
|
<Menu />
|
||||||
<CitySelector />
|
</IconButton>
|
||||||
|
<CitySelector />
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex gap-2 items-center">
|
<div className="flex gap-2 items-center">
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
{(() => {
|
{(() => {
|
||||||
@@ -114,7 +117,7 @@ export const Layout: React.FC<LayoutProps> = observer(({ children }) => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src="/favicon_ship.png"
|
src="/favicon_ship.svg"
|
||||||
alt="logo"
|
alt="logo"
|
||||||
width={40}
|
width={40}
|
||||||
height={40}
|
height={40}
|
||||||
|
|||||||
@@ -3,11 +3,9 @@ import { OrbitControls, Stage, useGLTF } from "@react-three/drei";
|
|||||||
import { useEffect, Suspense } from "react";
|
import { useEffect, Suspense } from "react";
|
||||||
import { Box, CircularProgress, Typography } from "@mui/material";
|
import { Box, CircularProgress, Typography } from "@mui/material";
|
||||||
|
|
||||||
// Утилита для очистки кеша GLTF
|
|
||||||
const clearGLTFCache = (url?: string) => {
|
const clearGLTFCache = (url?: string) => {
|
||||||
try {
|
try {
|
||||||
if (url) {
|
if (url) {
|
||||||
// Если это blob URL, очищаем его из кеша
|
|
||||||
if (url.startsWith("blob:")) {
|
if (url.startsWith("blob:")) {
|
||||||
useGLTF.clear(url);
|
useGLTF.clear(url);
|
||||||
} else {
|
} else {
|
||||||
@@ -19,29 +17,23 @@ const clearGLTFCache = (url?: string) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Утилита для проверки типа файла
|
|
||||||
const isValid3DFile = (url: string): boolean => {
|
const isValid3DFile = (url: string): boolean => {
|
||||||
try {
|
try {
|
||||||
const urlObj = new URL(url);
|
const urlObj = new URL(url);
|
||||||
const pathname = urlObj.pathname.toLowerCase();
|
const pathname = urlObj.pathname.toLowerCase();
|
||||||
const searchParams = urlObj.searchParams;
|
const searchParams = urlObj.searchParams;
|
||||||
|
|
||||||
// Проверяем расширение файла в пути
|
|
||||||
const validExtensions = [".glb", ".gltf"];
|
const validExtensions = [".glb", ".gltf"];
|
||||||
const hasValidExtension = validExtensions.some((ext) =>
|
const hasValidExtension = validExtensions.some((ext) =>
|
||||||
pathname.endsWith(ext)
|
pathname.endsWith(ext)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Проверяем параметры запроса на наличие типа файла
|
|
||||||
const fileType = searchParams.get("type") || searchParams.get("format");
|
const fileType = searchParams.get("type") || searchParams.get("format");
|
||||||
const hasValidType =
|
const hasValidType =
|
||||||
fileType && ["glb", "gltf"].includes(fileType.toLowerCase());
|
fileType && ["glb", "gltf"].includes(fileType.toLowerCase());
|
||||||
|
|
||||||
// Если это blob URL, считаем его валидным (пользователь выбрал файл)
|
|
||||||
const isBlobUrl = url.startsWith("blob:");
|
const isBlobUrl = url.startsWith("blob:");
|
||||||
|
|
||||||
// Если это URL с токеном и нет явного расширения, считаем валидным
|
|
||||||
// (предполагаем что сервер вернет правильный файл)
|
|
||||||
const hasToken = searchParams.has("token");
|
const hasToken = searchParams.has("token");
|
||||||
const isServerUrl = hasToken && !hasValidExtension;
|
const isServerUrl = hasToken && !hasValidExtension;
|
||||||
|
|
||||||
@@ -51,7 +43,7 @@ const isValid3DFile = (url: string): boolean => {
|
|||||||
return isValid;
|
return isValid;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn("⚠️ isValid3DFile: Ошибка при проверке URL", error);
|
console.warn("⚠️ isValid3DFile: Ошибка при проверке URL", error);
|
||||||
// В случае ошибки парсинга URL, считаем валидным (пусть useGLTF сам разберется)
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -63,13 +55,10 @@ type ModelViewerProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const Model = ({ fileUrl }: { fileUrl: string }) => {
|
const Model = ({ fileUrl }: { fileUrl: string }) => {
|
||||||
// Очищаем кеш перед загрузкой новой модели
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Очищаем кеш для текущего URL
|
|
||||||
clearGLTFCache(fileUrl);
|
clearGLTFCache(fileUrl);
|
||||||
}, [fileUrl]);
|
}, [fileUrl]);
|
||||||
|
|
||||||
// Проверяем валидность файла перед загрузкой (только для blob URL)
|
|
||||||
if (fileUrl.startsWith("blob:") && !isValid3DFile(fileUrl)) {
|
if (fileUrl.startsWith("blob:") && !isValid3DFile(fileUrl)) {
|
||||||
console.warn("⚠️ Model: Попытка загрузить невалидный 3D файл", { fileUrl });
|
console.warn("⚠️ Model: Попытка загрузить невалидный 3D файл", { fileUrl });
|
||||||
throw new Error(`Файл не является корректной 3D моделью: ${fileUrl}`);
|
throw new Error(`Файл не является корректной 3D моделью: ${fileUrl}`);
|
||||||
@@ -114,16 +103,13 @@ export const ThreeView = ({
|
|||||||
height = "100%",
|
height = "100%",
|
||||||
width = "100%",
|
width = "100%",
|
||||||
}: ModelViewerProps) => {
|
}: ModelViewerProps) => {
|
||||||
// Проверяем валидность файла (только для blob URL)
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (fileUrl.startsWith("blob:") && !isValid3DFile(fileUrl)) {
|
if (fileUrl.startsWith("blob:") && !isValid3DFile(fileUrl)) {
|
||||||
console.warn("⚠️ ThreeView: Невалидный 3D файл", { fileUrl });
|
console.warn("⚠️ ThreeView: Невалидный 3D файл", { fileUrl });
|
||||||
}
|
}
|
||||||
}, [fileUrl]);
|
}, [fileUrl]);
|
||||||
|
|
||||||
// Очищаем кеш при размонтировании и при смене URL
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Очищаем кеш сразу при монтировании компонента
|
|
||||||
clearGLTFCache(fileUrl);
|
clearGLTFCache(fileUrl);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ export class ThreeViewErrorBoundary extends Component<Props, State> {
|
|||||||
props: Props,
|
props: Props,
|
||||||
state: State
|
state: State
|
||||||
): Partial<State> | null {
|
): Partial<State> | null {
|
||||||
// Сбрасываем ошибку ТОЛЬКО при смене медиа (когда меняется ID в resetKey)
|
|
||||||
if (
|
if (
|
||||||
props.resetKey !== state.lastResetKey &&
|
props.resetKey !== state.lastResetKey &&
|
||||||
state.lastResetKey !== undefined
|
state.lastResetKey !== undefined
|
||||||
@@ -43,7 +42,6 @@ export class ThreeViewErrorBoundary extends Component<Props, State> {
|
|||||||
const oldMediaId = String(state.lastResetKey).split("-")[0];
|
const oldMediaId = String(state.lastResetKey).split("-")[0];
|
||||||
const newMediaId = String(props.resetKey).split("-")[0];
|
const newMediaId = String(props.resetKey).split("-")[0];
|
||||||
|
|
||||||
// Сбрасываем ошибку только если изменился ID медиа (пользователь выбрал другую модель)
|
|
||||||
if (oldMediaId !== newMediaId) {
|
if (oldMediaId !== newMediaId) {
|
||||||
return {
|
return {
|
||||||
hasError: false,
|
hasError: false,
|
||||||
@@ -52,9 +50,6 @@ export class ThreeViewErrorBoundary extends Component<Props, State> {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Если изменился только счетчик (нажата кнопка "Попробовать снова"), обновляем lastResetKey
|
|
||||||
// но не сбрасываем ошибку автоматически - ждем результата загрузки
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
lastResetKey: props.resetKey,
|
lastResetKey: props.resetKey,
|
||||||
};
|
};
|
||||||
@@ -127,15 +122,12 @@ export class ThreeViewErrorBoundary extends Component<Props, State> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
handleReset = () => {
|
handleReset = () => {
|
||||||
// Сначала сбрасываем состояние ошибки
|
|
||||||
this.setState(
|
this.setState(
|
||||||
{
|
{
|
||||||
hasError: false,
|
hasError: false,
|
||||||
error: null,
|
error: null,
|
||||||
},
|
},
|
||||||
() => {
|
() => {
|
||||||
// После того как состояние обновилось, вызываем callback для изменения resetKey
|
|
||||||
// Это приведет к пересозданию компонента и новой попытке загрузки
|
|
||||||
this.props.onReset?.();
|
this.props.onReset?.();
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ export function MediaViewer({
|
|||||||
// Используем новый cache manager для очистки кеша
|
// Используем новый cache manager для очистки кеша
|
||||||
clearMediaTransitionCache(
|
clearMediaTransitionCache(
|
||||||
previousMediaId,
|
previousMediaId,
|
||||||
media?.id || null,
|
|
||||||
media?.media_type
|
media?.media_type
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,158 +0,0 @@
|
|||||||
// import { Box, Button, Paper, Typography } from "@mui/material";
|
|
||||||
// import { X, Upload } from "lucide-react";
|
|
||||||
// import { useCallback, useState } from "react";
|
|
||||||
// import { useDropzone } from "react-dropzone";
|
|
||||||
// import { UploadMediaDialog } from "@shared";
|
|
||||||
// import { createSightStore } from "@shared";
|
|
||||||
|
|
||||||
// interface MediaUploadBoxProps {
|
|
||||||
// title: string;
|
|
||||||
// tooltip?: string;
|
|
||||||
// mediaId: string | null;
|
|
||||||
// onMediaSelect: (mediaId: string) => void;
|
|
||||||
// onMediaRemove: () => void;
|
|
||||||
// onPreviewClick: (mediaId: string) => void;
|
|
||||||
// token: string;
|
|
||||||
// type: "thumbnail" | "watermark_lu" | "watermark_rd";
|
|
||||||
// }
|
|
||||||
|
|
||||||
// export const MediaUploadBox = ({
|
|
||||||
// title,
|
|
||||||
// tooltip,
|
|
||||||
// mediaId,
|
|
||||||
// onMediaSelect,
|
|
||||||
// onMediaRemove,
|
|
||||||
// onPreviewClick,
|
|
||||||
// token,
|
|
||||||
// type,
|
|
||||||
// }: MediaUploadBoxProps) => {
|
|
||||||
// const [uploadMediaOpen, setUploadMediaOpen] = useState(false);
|
|
||||||
// const [fileToUpload, setFileToUpload] = useState<File | null>(null);
|
|
||||||
|
|
||||||
// const onDrop = useCallback((acceptedFiles: File[]) => {
|
|
||||||
// if (acceptedFiles.length > 0) {
|
|
||||||
// setFileToUpload(acceptedFiles[0]);
|
|
||||||
// setUploadMediaOpen(true);
|
|
||||||
// }
|
|
||||||
// }, []);
|
|
||||||
|
|
||||||
// const { getRootProps, getInputProps, isDragActive } = useDropzone({
|
|
||||||
// onDrop,
|
|
||||||
// accept: {
|
|
||||||
// "image/*": [".png", ".jpg", ".jpeg", ".gif"],
|
|
||||||
// },
|
|
||||||
// multiple: false,
|
|
||||||
// });
|
|
||||||
|
|
||||||
// const handleUploadComplete = async (media: {
|
|
||||||
// id: string;
|
|
||||||
// filename: string;
|
|
||||||
// media_name?: string;
|
|
||||||
// media_type: number;
|
|
||||||
// }) => {
|
|
||||||
// onMediaSelect(media.id);
|
|
||||||
// };
|
|
||||||
|
|
||||||
// return (
|
|
||||||
// <>
|
|
||||||
// <Paper
|
|
||||||
// elevation={2}
|
|
||||||
// sx={{
|
|
||||||
// padding: 2,
|
|
||||||
// display: "flex",
|
|
||||||
// flexDirection: "column",
|
|
||||||
// alignItems: "center",
|
|
||||||
// gap: 1,
|
|
||||||
// flex: 1,
|
|
||||||
// minWidth: 150,
|
|
||||||
// }}
|
|
||||||
// >
|
|
||||||
// <Box sx={{ display: "flex", alignItems: "center" }}>
|
|
||||||
// <Typography variant="subtitle2" gutterBottom sx={{ mb: 0, mr: 0.5 }}>
|
|
||||||
// {title}
|
|
||||||
// </Typography>
|
|
||||||
// </Box>
|
|
||||||
// <Box
|
|
||||||
// {...getRootProps()}
|
|
||||||
// sx={{
|
|
||||||
// position: "relative",
|
|
||||||
// width: "200px",
|
|
||||||
// height: "200px",
|
|
||||||
// display: "flex",
|
|
||||||
// alignItems: "center",
|
|
||||||
// justifyContent: "center",
|
|
||||||
// borderRadius: 1,
|
|
||||||
// mb: 1,
|
|
||||||
// cursor: mediaId ? "pointer" : "default",
|
|
||||||
// border: isDragActive ? "2px dashed #1976d2" : "none",
|
|
||||||
// backgroundColor: isDragActive
|
|
||||||
// ? "rgba(25, 118, 210, 0.04)"
|
|
||||||
// : "transparent",
|
|
||||||
// transition: "all 0.2s ease",
|
|
||||||
// }}
|
|
||||||
// >
|
|
||||||
// <input {...getInputProps()} />
|
|
||||||
// {mediaId && (
|
|
||||||
// <button
|
|
||||||
// className="absolute top-2 right-2 z-10"
|
|
||||||
// onClick={(e) => {
|
|
||||||
// e.stopPropagation();
|
|
||||||
// onMediaRemove();
|
|
||||||
// }}
|
|
||||||
// >
|
|
||||||
// <X color="red" />
|
|
||||||
// </button>
|
|
||||||
// )}
|
|
||||||
// {mediaId ? (
|
|
||||||
// <img
|
|
||||||
// src={`${
|
|
||||||
// import.meta.env.VITE_KRBL_MEDIA
|
|
||||||
// }${mediaId}/download?token=${token}`}
|
|
||||||
// alt={title}
|
|
||||||
// style={{ maxWidth: "100%", maxHeight: "100%" }}
|
|
||||||
// onClick={(e) => {
|
|
||||||
// e.stopPropagation();
|
|
||||||
// onPreviewClick(mediaId);
|
|
||||||
// }}
|
|
||||||
// />
|
|
||||||
// ) : (
|
|
||||||
// <div className="w-full flex flex-col items-center justify-center gap-3">
|
|
||||||
// <div
|
|
||||||
// className={`w-full h-20 border rounded-md flex flex-col items-center transition-all duration-300 justify-center border-dashed ${
|
|
||||||
// isDragActive
|
|
||||||
// ? "border-blue-500 bg-blue-50"
|
|
||||||
// : "border-gray-300"
|
|
||||||
// } cursor-pointer hover:bg-gray-100`}
|
|
||||||
// >
|
|
||||||
// <Upload size={24} className="mb-2" />
|
|
||||||
// <p>
|
|
||||||
// {isDragActive ? "Отпустите файл здесь" : "Перетащите файл"}
|
|
||||||
// </p>
|
|
||||||
// </div>
|
|
||||||
// <p>или</p>
|
|
||||||
// <Button
|
|
||||||
// variant="contained"
|
|
||||||
// color="primary"
|
|
||||||
// onClick={(e) => {
|
|
||||||
// e.stopPropagation();
|
|
||||||
// onMediaSelect("");
|
|
||||||
// }}
|
|
||||||
// >
|
|
||||||
// Выбрать файл
|
|
||||||
// </Button>
|
|
||||||
// </div>
|
|
||||||
// )}
|
|
||||||
// </Box>
|
|
||||||
// </Paper>
|
|
||||||
|
|
||||||
// <UploadMediaDialog
|
|
||||||
// open={uploadMediaOpen}
|
|
||||||
// onClose={() => {
|
|
||||||
// setUploadMediaOpen(false);
|
|
||||||
// setFileToUpload(null);
|
|
||||||
// }}
|
|
||||||
// afterUpload={handleUploadComplete}
|
|
||||||
// />
|
|
||||||
// </>
|
|
||||||
// );
|
|
||||||
// };
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
// @widgets/LeftWidgetTab.tsx
|
|
||||||
import { Box, Button, TextField, Paper, Typography } from "@mui/material";
|
import { Box, Button, TextField, Paper, Typography } from "@mui/material";
|
||||||
import {
|
import {
|
||||||
BackButton,
|
BackButton,
|
||||||
@@ -50,17 +49,6 @@ export const CreateLeftTab = observer(
|
|||||||
const [isSelectMediaDialogOpen, setIsSelectMediaDialogOpen] =
|
const [isSelectMediaDialogOpen, setIsSelectMediaDialogOpen] =
|
||||||
useState(false);
|
useState(false);
|
||||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||||
// const handleMediaSelected = useCallback(() => {
|
|
||||||
// // При выборе медиа, обновляем данные для ТЕКУЩЕГО ЯЗЫКА
|
|
||||||
// // сохраняя текущие heading и body.
|
|
||||||
// updateSightInfo(language, {
|
|
||||||
// left: {
|
|
||||||
// heading: data.left.heading,
|
|
||||||
// body: data.left.body,
|
|
||||||
// },
|
|
||||||
// });
|
|
||||||
// setIsSelectMediaDialogOpen(false);
|
|
||||||
// }, [language, data.left.heading, data.left.body]);
|
|
||||||
|
|
||||||
const handleCloseArticleDialog = useCallback(() => {
|
const handleCloseArticleDialog = useCallback(() => {
|
||||||
setIsSelectArticleDialogOpen(false);
|
setIsSelectArticleDialogOpen(false);
|
||||||
|
|||||||
@@ -13,28 +13,27 @@ import {
|
|||||||
languageStore,
|
languageStore,
|
||||||
SelectArticleModal,
|
SelectArticleModal,
|
||||||
TabPanel,
|
TabPanel,
|
||||||
SelectMediaDialog, // Import
|
SelectMediaDialog,
|
||||||
UploadMediaDialog,
|
UploadMediaDialog,
|
||||||
Media, // Import
|
Media,
|
||||||
} from "@shared";
|
} from "@shared";
|
||||||
import {
|
import {
|
||||||
LanguageSwitcher,
|
LanguageSwitcher,
|
||||||
MediaArea, // Import
|
MediaArea,
|
||||||
MediaAreaForSight, // Import
|
MediaAreaForSight,
|
||||||
ReactMarkdownComponent,
|
ReactMarkdownComponent,
|
||||||
ReactMarkdownEditor,
|
ReactMarkdownEditor,
|
||||||
DeleteModal,
|
DeleteModal,
|
||||||
} from "@widgets";
|
} from "@widgets";
|
||||||
import { ImagePlus, Plus, Save, Trash2, Unlink, X } from "lucide-react"; // Import X
|
import { ImagePlus, Plus, Save, Trash2, Unlink, X } from "lucide-react";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { useState, useEffect } from "react"; // Added useEffect
|
import { useState, useEffect } from "react";
|
||||||
import { MediaViewer } from "../../MediaViewer/index";
|
import { MediaViewer } from "../../MediaViewer/index";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
import { authInstance } from "@shared";
|
import { authInstance } from "@shared";
|
||||||
import { DragDropContext, Droppable, Draggable } from "@hello-pangea/dnd";
|
import { DragDropContext, Droppable, Draggable } from "@hello-pangea/dnd";
|
||||||
|
|
||||||
type MediaItemShared = {
|
type MediaItemShared = {
|
||||||
// Define if not already available from @shared
|
|
||||||
id: string;
|
id: string;
|
||||||
filename: string;
|
filename: string;
|
||||||
media_name?: string;
|
media_name?: string;
|
||||||
@@ -52,14 +51,14 @@ export const CreateRightTab = observer(
|
|||||||
unlinkPreviewMedia,
|
unlinkPreviewMedia,
|
||||||
createLinkWithRightArticle,
|
createLinkWithRightArticle,
|
||||||
deleteRightArticleMedia,
|
deleteRightArticleMedia,
|
||||||
setFileToUpload, // From store
|
setFileToUpload,
|
||||||
setUploadMediaOpen, // From store
|
setUploadMediaOpen,
|
||||||
uploadMediaOpen, // From store
|
uploadMediaOpen,
|
||||||
unlinkRightAritcle, // Corrected spelling
|
unlinkRightAritcle,
|
||||||
deleteRightArticle,
|
deleteRightArticle,
|
||||||
linkExistingRightArticle,
|
linkExistingRightArticle,
|
||||||
createSight,
|
createSight,
|
||||||
clearCreateSight, // For resetting form
|
clearCreateSight,
|
||||||
updateRightArticles,
|
updateRightArticles,
|
||||||
} = createSightStore;
|
} = createSightStore;
|
||||||
const { language } = languageStore;
|
const { language } = languageStore;
|
||||||
@@ -78,7 +77,7 @@ export const CreateRightTab = observer(
|
|||||||
>(null);
|
>(null);
|
||||||
|
|
||||||
const [previewMedia, setPreviewMedia] = useState<Media | null>(null);
|
const [previewMedia, setPreviewMedia] = useState<Media | null>(null);
|
||||||
// Reset activeArticleIndex if language changes and index is out of bounds
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (sight.preview_media) {
|
if (sight.preview_media) {
|
||||||
const fetchMedia = async () => {
|
const fetchMedia = async () => {
|
||||||
@@ -97,7 +96,7 @@ export const CreateRightTab = observer(
|
|||||||
activeArticleIndex >= sight[language].right.length
|
activeArticleIndex >= sight[language].right.length
|
||||||
) {
|
) {
|
||||||
setActiveArticleIndex(null);
|
setActiveArticleIndex(null);
|
||||||
setType("media"); // Default back to media preview if selected article disappears
|
setType("media");
|
||||||
}
|
}
|
||||||
}, [language, sight[language].right, activeArticleIndex]);
|
}, [language, sight[language].right, activeArticleIndex]);
|
||||||
|
|
||||||
@@ -113,10 +112,9 @@ export const CreateRightTab = observer(
|
|||||||
try {
|
try {
|
||||||
await createSight(language);
|
await createSight(language);
|
||||||
toast.success("Достопримечательность успешно создана!");
|
toast.success("Достопримечательность успешно создана!");
|
||||||
clearCreateSight(); // Reset form
|
clearCreateSight();
|
||||||
setActiveArticleIndex(null);
|
setActiveArticleIndex(null);
|
||||||
setType("media");
|
setType("media");
|
||||||
// Potentially navigate away: history.push('/sights-list');
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to save sight:", error);
|
console.error("Failed to save sight:", error);
|
||||||
toast.error("Ошибка при создании достопримечательности.");
|
toast.error("Ошибка при создании достопримечательности.");
|
||||||
@@ -132,7 +130,7 @@ export const CreateRightTab = observer(
|
|||||||
handleCloseMenu();
|
handleCloseMenu();
|
||||||
try {
|
try {
|
||||||
const newArticleId = await createNewRightArticle();
|
const newArticleId = await createNewRightArticle();
|
||||||
// Automatically select the new article if ID is returned
|
|
||||||
const newIndex = sight[language].right.findIndex(
|
const newIndex = sight[language].right.findIndex(
|
||||||
(a) => a.id === newArticleId
|
(a) => a.id === newArticleId
|
||||||
);
|
);
|
||||||
@@ -140,7 +138,6 @@ export const CreateRightTab = observer(
|
|||||||
setActiveArticleIndex(newIndex);
|
setActiveArticleIndex(newIndex);
|
||||||
setType("article");
|
setType("article");
|
||||||
} else {
|
} else {
|
||||||
// Fallback if findIndex fails (should not happen if store updates correctly)
|
|
||||||
setActiveArticleIndex(sight[language].right.length - 1);
|
setActiveArticleIndex(sight[language].right.length - 1);
|
||||||
setType("article");
|
setType("article");
|
||||||
}
|
}
|
||||||
@@ -156,7 +153,7 @@ export const CreateRightTab = observer(
|
|||||||
const linkedArticleId = await linkExistingRightArticle(
|
const linkedArticleId = await linkExistingRightArticle(
|
||||||
selectedArticleId
|
selectedArticleId
|
||||||
);
|
);
|
||||||
setSelectArticleDialogOpen(false); // Close dialog
|
setSelectArticleDialogOpen(false);
|
||||||
const newIndex = sight[language].right.findIndex(
|
const newIndex = sight[language].right.findIndex(
|
||||||
(a) => a.id === linkedArticleId
|
(a) => a.id === linkedArticleId
|
||||||
);
|
);
|
||||||
@@ -174,7 +171,6 @@ export const CreateRightTab = observer(
|
|||||||
? sight[language].right[activeArticleIndex]
|
? sight[language].right[activeArticleIndex]
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
// Media Handling for Dialogs
|
|
||||||
const handleOpenUploadMedia = () => {
|
const handleOpenUploadMedia = () => {
|
||||||
setUploadMediaOpen(true);
|
setUploadMediaOpen(true);
|
||||||
};
|
};
|
||||||
@@ -203,7 +199,6 @@ export const CreateRightTab = observer(
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleMediaUploaded = async (media: MediaItemShared) => {
|
const handleMediaUploaded = async (media: MediaItemShared) => {
|
||||||
// After UploadMediaDialog finishes
|
|
||||||
setUploadMediaOpen(false);
|
setUploadMediaOpen(false);
|
||||||
setFileToUpload(null);
|
setFileToUpload(null);
|
||||||
if (mediaTarget === "sightPreview") {
|
if (mediaTarget === "sightPreview") {
|
||||||
@@ -211,36 +206,25 @@ export const CreateRightTab = observer(
|
|||||||
} else if (mediaTarget === "rightArticle" && currentRightArticle) {
|
} else if (mediaTarget === "rightArticle" && currentRightArticle) {
|
||||||
await createLinkWithRightArticle(media, currentRightArticle.id);
|
await createLinkWithRightArticle(media, currentRightArticle.id);
|
||||||
}
|
}
|
||||||
setMediaTarget(null); // Reset target
|
setMediaTarget(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDragEnd = (result: any) => {
|
const handleDragEnd = (result: any) => {
|
||||||
const { source, destination } = result;
|
const { source, destination } = result;
|
||||||
|
|
||||||
// 1. Guard clause: If dropped outside any droppable area, do nothing.
|
|
||||||
if (!destination) return;
|
if (!destination) return;
|
||||||
|
|
||||||
// Extract source and destination indices
|
|
||||||
const sourceIndex = source.index;
|
const sourceIndex = source.index;
|
||||||
const destinationIndex = destination.index;
|
const destinationIndex = destination.index;
|
||||||
|
|
||||||
// 2. Guard clause: If dropped in the same position, do nothing.
|
|
||||||
if (sourceIndex === destinationIndex) return;
|
if (sourceIndex === destinationIndex) return;
|
||||||
|
|
||||||
// 3. Create a new array with reordered articles:
|
|
||||||
// - Create a shallow copy of the current articles array.
|
|
||||||
// This is important for immutability and triggering re-renders.
|
|
||||||
const newRightArticles = [...sight[language].right];
|
const newRightArticles = [...sight[language].right];
|
||||||
|
|
||||||
// - Remove the dragged article from its original position.
|
|
||||||
// `splice` returns an array of removed items, so we destructure the first (and only) one.
|
|
||||||
const [movedArticle] = newRightArticles.splice(sourceIndex, 1);
|
const [movedArticle] = newRightArticles.splice(sourceIndex, 1);
|
||||||
|
|
||||||
// - Insert the moved article into its new position.
|
|
||||||
newRightArticles.splice(destinationIndex, 0, movedArticle);
|
newRightArticles.splice(destinationIndex, 0, movedArticle);
|
||||||
|
|
||||||
// 4. Update the store with the new order:
|
|
||||||
// This will typically trigger a re-render of the component with the updated list.
|
|
||||||
updateRightArticles(newRightArticles);
|
updateRightArticles(newRightArticles);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -254,7 +238,7 @@ export const CreateRightTab = observer(
|
|||||||
height: "100%",
|
height: "100%",
|
||||||
minHeight: "calc(100vh - 200px)",
|
minHeight: "calc(100vh - 200px)",
|
||||||
gap: 2,
|
gap: 2,
|
||||||
paddingBottom: "70px", // Space for the save button
|
paddingBottom: "70px",
|
||||||
position: "relative",
|
position: "relative",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -264,7 +248,6 @@ export const CreateRightTab = observer(
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Box sx={{ display: "flex", flexGrow: 1, gap: 2.5 }}>
|
<Box sx={{ display: "flex", flexGrow: 1, gap: 2.5 }}>
|
||||||
{/* Left Column: Navigation & Article List */}
|
|
||||||
<Box className="flex flex-col w-[75%] gap-2">
|
<Box className="flex flex-col w-[75%] gap-2">
|
||||||
<Box className="w-full flex gap-2 ">
|
<Box className="w-full flex gap-2 ">
|
||||||
<Box className="relative w-[20%] h-[70vh] flex flex-col rounded-2xl overflow-y-auto gap-3 border border-gray-300 p-3">
|
<Box className="relative w-[20%] h-[70vh] flex flex-col rounded-2xl overflow-y-auto gap-3 border border-gray-300 p-3">
|
||||||
@@ -272,7 +255,6 @@ export const CreateRightTab = observer(
|
|||||||
<Box
|
<Box
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setType("media");
|
setType("media");
|
||||||
// setActiveArticleIndex(null); // Optional: deselect article when switching to general media view
|
|
||||||
}}
|
}}
|
||||||
className={`w-full p-4 rounded-2xl cursor-pointer text-sm hover:bg-gray-300 transition-all duration-300 ${
|
className={`w-full p-4 rounded-2xl cursor-pointer text-sm hover:bg-gray-300 transition-all duration-300 ${
|
||||||
type === "media"
|
type === "media"
|
||||||
@@ -364,7 +346,6 @@ export const CreateRightTab = observer(
|
|||||||
</Menu>
|
</Menu>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* Main content area: Article Editor or Sight Media Preview */}
|
|
||||||
{type === "article" && currentRightArticle ? (
|
{type === "article" && currentRightArticle ? (
|
||||||
<Box className="w-[80%] border border-gray-300 p-3 flex flex-col gap-2 overflow-hidden">
|
<Box className="w-[80%] border border-gray-300 p-3 flex flex-col gap-2 overflow-hidden">
|
||||||
<Box className="flex justify-end gap-2 mb-1 flex-shrink-0">
|
<Box className="flex justify-end gap-2 mb-1 flex-shrink-0">
|
||||||
@@ -375,7 +356,7 @@ export const CreateRightTab = observer(
|
|||||||
startIcon={<Unlink color="white" size={18} />}
|
startIcon={<Unlink color="white" size={18} />}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (currentRightArticle) {
|
if (currentRightArticle) {
|
||||||
unlinkRightAritcle(currentRightArticle.id); // Corrected function name
|
unlinkRightAritcle(currentRightArticle.id);
|
||||||
setActiveArticleIndex(null);
|
setActiveArticleIndex(null);
|
||||||
setType("media");
|
setType("media");
|
||||||
}
|
}
|
||||||
@@ -435,7 +416,7 @@ export const CreateRightTab = observer(
|
|||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
<MediaArea
|
<MediaArea
|
||||||
articleId={currentRightArticle.id} // Needs a real ID
|
articleId={currentRightArticle.id}
|
||||||
mediaIds={currentRightArticle.media || []}
|
mediaIds={currentRightArticle.media || []}
|
||||||
onFilesDrop={(files) => {
|
onFilesDrop={(files) => {
|
||||||
if (files.length > 0) {
|
if (files.length > 0) {
|
||||||
@@ -507,7 +488,6 @@ export const CreateRightTab = observer(
|
|||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* Right Column: Live Preview */}
|
|
||||||
<Box className="w-[25%] mr-10">
|
<Box className="w-[25%] mr-10">
|
||||||
{type === "article" && activeArticleIndex !== null && (
|
{type === "article" && activeArticleIndex !== null && (
|
||||||
<Paper
|
<Paper
|
||||||
@@ -662,12 +642,11 @@ export const CreateRightTab = observer(
|
|||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* Sticky Save Button Footer */}
|
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
bottom: "-20px",
|
bottom: "-20px",
|
||||||
left: 0, // ensure it spans from left
|
left: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
padding: 2,
|
padding: 2,
|
||||||
backgroundColor: "background.paper",
|
backgroundColor: "background.paper",
|
||||||
@@ -689,19 +668,17 @@ export const CreateRightTab = observer(
|
|||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* Modals */}
|
|
||||||
<SelectArticleModal
|
<SelectArticleModal
|
||||||
open={selectArticleDialogOpen}
|
open={selectArticleDialogOpen}
|
||||||
onClose={() => setSelectArticleDialogOpen(false)}
|
onClose={() => setSelectArticleDialogOpen(false)}
|
||||||
onSelectArticle={handleSelectExistingArticleAndLink}
|
onSelectArticle={handleSelectExistingArticleAndLink}
|
||||||
// Pass IDs of already linked/added right articles to exclude them from selection
|
|
||||||
linkedArticleIds={sight[language].right.map((article) => article.id)}
|
linkedArticleIds={sight[language].right.map((article) => article.id)}
|
||||||
/>
|
/>
|
||||||
<UploadMediaDialog
|
<UploadMediaDialog
|
||||||
open={uploadMediaOpen} // From store
|
open={uploadMediaOpen}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
setUploadMediaOpen(false);
|
setUploadMediaOpen(false);
|
||||||
setFileToUpload(null); // Clear file if dialog is closed without upload
|
setFileToUpload(null);
|
||||||
setMediaTarget(null);
|
setMediaTarget(null);
|
||||||
}}
|
}}
|
||||||
contextObjectName={sight[language].name}
|
contextObjectName={sight[language].name}
|
||||||
@@ -712,7 +689,7 @@ export const CreateRightTab = observer(
|
|||||||
? sight[language].right[activeArticleIndex].heading
|
? sight[language].right[activeArticleIndex].heading
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
afterUpload={handleMediaUploaded} // This will use the mediaTarget
|
afterUpload={handleMediaUploaded}
|
||||||
/>
|
/>
|
||||||
<SelectMediaDialog
|
<SelectMediaDialog
|
||||||
open={isSelectMediaDialogOpen}
|
open={isSelectMediaDialogOpen}
|
||||||
|
|||||||
@@ -37,8 +37,8 @@ import { useEffect, useState } from "react";
|
|||||||
|
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
|
|
||||||
// Компонент предупреждающего окна (перенесен сюда)
|
|
||||||
import { SaveWithoutCityAgree } from "@widgets";
|
import { SaveWithoutCityAgree } from "@widgets";
|
||||||
|
import { LinkedStations } from "@pages";
|
||||||
|
|
||||||
export const InformationTab = observer(
|
export const InformationTab = observer(
|
||||||
({ value, index }: { value: number; index: number }) => {
|
({ value, index }: { value: number; index: number }) => {
|
||||||
@@ -62,7 +62,7 @@ export const InformationTab = observer(
|
|||||||
"thumbnail" | "watermark_lu" | "watermark_rd" | "video_preview" | null
|
"thumbnail" | "watermark_lu" | "watermark_rd" | "video_preview" | null
|
||||||
>(null);
|
>(null);
|
||||||
const { cities } = cityStore;
|
const { cities } = cityStore;
|
||||||
// НОВОЕ СОСТОЯНИЕ ДЛЯ ПРЕДУПРЕЖДАЮЩЕГО ОКНА
|
|
||||||
const [isSaveWarningOpen, setIsSaveWarningOpen] = useState(false);
|
const [isSaveWarningOpen, setIsSaveWarningOpen] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {}, [hardcodeType]);
|
useEffect(() => {}, [hardcodeType]);
|
||||||
@@ -119,16 +119,14 @@ export const InformationTab = observer(
|
|||||||
updateSightInfo(language, content, common);
|
updateSightInfo(language, content, common);
|
||||||
};
|
};
|
||||||
|
|
||||||
// НОВАЯ ФУНКЦИЯ: Фактическое сохранение (вызывается после подтверждения)
|
|
||||||
const executeSave = async () => {
|
const executeSave = async () => {
|
||||||
await updateSight();
|
await updateSight();
|
||||||
toast.success("Достопримечательность сохранена");
|
toast.success("Достопримечательность сохранена");
|
||||||
};
|
};
|
||||||
|
|
||||||
// ОБНОВЛЕННАЯ ФУНКЦИЯ: Проверка и вызов окна или сохранения
|
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
const isCityMissing = !sight.common.city_id;
|
const isCityMissing = !sight.common.city_id;
|
||||||
// Проверяем названия на всех языках
|
|
||||||
const isNameMissing = !sight.ru.name || !sight.en.name || !sight.zh.name;
|
const isNameMissing = !sight.ru.name || !sight.en.name || !sight.zh.name;
|
||||||
|
|
||||||
if (isCityMissing || isNameMissing) {
|
if (isCityMissing || isNameMissing) {
|
||||||
@@ -139,13 +137,11 @@ export const InformationTab = observer(
|
|||||||
await executeSave();
|
await executeSave();
|
||||||
};
|
};
|
||||||
|
|
||||||
// Обработчик "Да" в предупреждающем окне
|
|
||||||
const handleConfirmSave = async () => {
|
const handleConfirmSave = async () => {
|
||||||
setIsSaveWarningOpen(false);
|
setIsSaveWarningOpen(false);
|
||||||
await executeSave();
|
await executeSave();
|
||||||
};
|
};
|
||||||
|
|
||||||
// Обработчик "Нет" в предупреждающем окне
|
|
||||||
const handleCancelSave = () => {
|
const handleCancelSave = () => {
|
||||||
setIsSaveWarningOpen(false);
|
setIsSaveWarningOpen(false);
|
||||||
};
|
};
|
||||||
@@ -275,6 +271,16 @@ export const InformationTab = observer(
|
|||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
<Box sx={{ width: "80%" }}>
|
||||||
|
{sight.common.id !== 0 && (
|
||||||
|
<LinkedStations
|
||||||
|
parentId={sight.common.id}
|
||||||
|
fields={[{ label: "Название", data: "name" }]}
|
||||||
|
type="edit"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
@@ -431,7 +437,7 @@ export const InformationTab = observer(
|
|||||||
variant="contained"
|
variant="contained"
|
||||||
color="success"
|
color="success"
|
||||||
startIcon={<Save color="white" size={18} />}
|
startIcon={<Save color="white" size={18} />}
|
||||||
onClick={handleSave} // Используем новую функцию-обработчик
|
onClick={handleSave}
|
||||||
>
|
>
|
||||||
Сохранить
|
Сохранить
|
||||||
</Button>
|
</Button>
|
||||||
@@ -538,7 +544,6 @@ export const InformationTab = observer(
|
|||||||
</Dialog>
|
</Dialog>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* ИНТЕГРИРОВАННОЕ ПРЕДУПРЕЖДАЮЩЕЕ ОКНО */}
|
|
||||||
{isSaveWarningOpen && (
|
{isSaveWarningOpen && (
|
||||||
<SaveWithoutCityAgree
|
<SaveWithoutCityAgree
|
||||||
blocker={{
|
blocker={{
|
||||||
@@ -550,4 +555,4 @@ export const InformationTab = observer(
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ export const RightWidgetTab = observer(
|
|||||||
try {
|
try {
|
||||||
const newArticleId = await createNewRightArticle();
|
const newArticleId = await createNewRightArticle();
|
||||||
handleClose();
|
handleClose();
|
||||||
// Automatically select the newly created article
|
|
||||||
const newIndex = sight[language].right.findIndex(
|
const newIndex = sight[language].right.findIndex(
|
||||||
(article) => article.id === newArticleId
|
(article) => article.id === newArticleId
|
||||||
);
|
);
|
||||||
@@ -144,7 +144,7 @@ export const RightWidgetTab = observer(
|
|||||||
try {
|
try {
|
||||||
const linkedArticleId = await linkArticle(id);
|
const linkedArticleId = await linkArticle(id);
|
||||||
handleCloseSelectModal();
|
handleCloseSelectModal();
|
||||||
// Automatically select the newly linked article
|
|
||||||
const newIndex = sight[language].right.findIndex(
|
const newIndex = sight[language].right.findIndex(
|
||||||
(article) => article.id === linkedArticleId
|
(article) => article.id === linkedArticleId
|
||||||
);
|
);
|
||||||
@@ -177,30 +177,19 @@ export const RightWidgetTab = observer(
|
|||||||
const handleDragEnd = (result: DropResult) => {
|
const handleDragEnd = (result: DropResult) => {
|
||||||
const { source, destination } = result;
|
const { source, destination } = result;
|
||||||
|
|
||||||
// 1. Guard clause: If dropped outside any droppable area, do nothing.
|
|
||||||
if (!destination) return;
|
if (!destination) return;
|
||||||
|
|
||||||
// Extract source and destination indices
|
|
||||||
const sourceIndex = source.index;
|
const sourceIndex = source.index;
|
||||||
const destinationIndex = destination.index;
|
const destinationIndex = destination.index;
|
||||||
|
|
||||||
// 2. Guard clause: If dropped in the same position, do nothing.
|
|
||||||
if (sourceIndex === destinationIndex) return;
|
if (sourceIndex === destinationIndex) return;
|
||||||
|
|
||||||
// 3. Create a new array with reordered articles:
|
|
||||||
// - Create a shallow copy of the current articles array.
|
|
||||||
// This is important for immutability and triggering re-renders.
|
|
||||||
const newRightArticles = [...sight[language].right];
|
const newRightArticles = [...sight[language].right];
|
||||||
|
|
||||||
// - Remove the dragged article from its original position.
|
|
||||||
// `splice` returns an array of removed items, so we destructure the first (and only) one.
|
|
||||||
const [movedArticle] = newRightArticles.splice(sourceIndex, 1);
|
const [movedArticle] = newRightArticles.splice(sourceIndex, 1);
|
||||||
|
|
||||||
// - Insert the moved article into its new position.
|
|
||||||
newRightArticles.splice(destinationIndex, 0, movedArticle);
|
newRightArticles.splice(destinationIndex, 0, movedArticle);
|
||||||
|
|
||||||
// 4. Update the store with the new order:
|
|
||||||
// This will typically trigger a re-render of the component with the updated list.
|
|
||||||
updateRightArticles(newRightArticles);
|
updateRightArticles(newRightArticles);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ interface VideoPreviewCardProps {
|
|||||||
onDeleteVideoClick: () => void;
|
onDeleteVideoClick: () => void;
|
||||||
onSelectVideoClick: (file?: File) => void;
|
onSelectVideoClick: (file?: File) => void;
|
||||||
tooltipText?: string;
|
tooltipText?: string;
|
||||||
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const VideoPreviewCard: React.FC<VideoPreviewCardProps> = ({
|
export const VideoPreviewCard: React.FC<VideoPreviewCardProps> = ({
|
||||||
@@ -20,15 +21,15 @@ export const VideoPreviewCard: React.FC<VideoPreviewCardProps> = ({
|
|||||||
onDeleteVideoClick,
|
onDeleteVideoClick,
|
||||||
onSelectVideoClick,
|
onSelectVideoClick,
|
||||||
tooltipText,
|
tooltipText,
|
||||||
|
className,
|
||||||
}) => {
|
}) => {
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
const [isDragOver, setIsDragOver] = useState(false);
|
const [isDragOver, setIsDragOver] = useState(false);
|
||||||
const token = localStorage.getItem("token");
|
const token = localStorage.getItem("token");
|
||||||
|
|
||||||
useEffect(() => {}, [isDragOver]);
|
useEffect(() => {}, [isDragOver]);
|
||||||
// --- Click to select file ---
|
|
||||||
const handleZoneClick = () => {
|
const handleZoneClick = () => {
|
||||||
// Trigger the hidden file input click
|
|
||||||
fileInputRef.current?.click();
|
fileInputRef.current?.click();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -38,19 +39,17 @@ export const VideoPreviewCard: React.FC<VideoPreviewCardProps> = ({
|
|||||||
const file = event.target.files?.[0];
|
const file = event.target.files?.[0];
|
||||||
if (file) {
|
if (file) {
|
||||||
if (file.type.startsWith("video/")) {
|
if (file.type.startsWith("video/")) {
|
||||||
// Открываем диалог загрузки медиа с файлом видео
|
|
||||||
onSelectVideoClick(file);
|
onSelectVideoClick(file);
|
||||||
} else {
|
} else {
|
||||||
toast.error("Пожалуйста, выберите видео файл");
|
toast.error("Пожалуйста, выберите видео файл");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Reset the input value so selecting the same file again triggers change
|
|
||||||
event.target.value = "";
|
event.target.value = "";
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- Drag and Drop Handlers ---
|
|
||||||
const handleDragOver = (event: DragEvent<HTMLDivElement>) => {
|
const handleDragOver = (event: DragEvent<HTMLDivElement>) => {
|
||||||
event.preventDefault(); // Crucial to allow a drop
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
setIsDragOver(true);
|
setIsDragOver(true);
|
||||||
};
|
};
|
||||||
@@ -62,7 +61,7 @@ export const VideoPreviewCard: React.FC<VideoPreviewCardProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleDrop = async (event: DragEvent<HTMLDivElement>) => {
|
const handleDrop = async (event: DragEvent<HTMLDivElement>) => {
|
||||||
event.preventDefault(); // Crucial to allow a drop
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
setIsDragOver(false);
|
setIsDragOver(false);
|
||||||
|
|
||||||
@@ -70,7 +69,6 @@ export const VideoPreviewCard: React.FC<VideoPreviewCardProps> = ({
|
|||||||
if (files && files.length > 0) {
|
if (files && files.length > 0) {
|
||||||
const file = files[0];
|
const file = files[0];
|
||||||
if (file.type.startsWith("video/")) {
|
if (file.type.startsWith("video/")) {
|
||||||
// Открываем диалог загрузки медиа с файлом видео
|
|
||||||
onSelectVideoClick(file);
|
onSelectVideoClick(file);
|
||||||
} else {
|
} else {
|
||||||
toast.error("Пожалуйста, выберите видео файл");
|
toast.error("Пожалуйста, выберите видео файл");
|
||||||
@@ -89,7 +87,10 @@ export const VideoPreviewCard: React.FC<VideoPreviewCardProps> = ({
|
|||||||
gap: 1,
|
gap: 1,
|
||||||
flex: 1,
|
flex: 1,
|
||||||
minWidth: 150,
|
minWidth: 150,
|
||||||
|
width: "min-content",
|
||||||
|
mx: "auto",
|
||||||
}}
|
}}
|
||||||
|
className={className}
|
||||||
>
|
>
|
||||||
<Box sx={{ display: "flex", alignItems: "center" }}>
|
<Box sx={{ display: "flex", alignItems: "center" }}>
|
||||||
<Typography variant="subtitle2" gutterBottom sx={{ mb: 0, mr: 0.5 }}>
|
<Typography variant="subtitle2" gutterBottom sx={{ mb: 0, mr: 0.5 }}>
|
||||||
@@ -127,7 +128,10 @@ export const VideoPreviewCard: React.FC<VideoPreviewCardProps> = ({
|
|||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{videoId ? (
|
{videoId ? (
|
||||||
<Box sx={{ position: "relative", width: "100%", height: "100%" }}>
|
<Box
|
||||||
|
sx={{ position: "relative", width: "100%", height: "100%" }}
|
||||||
|
className={className}
|
||||||
|
>
|
||||||
<video
|
<video
|
||||||
src={`${
|
src={`${
|
||||||
import.meta.env.VITE_KRBL_MEDIA
|
import.meta.env.VITE_KRBL_MEDIA
|
||||||
@@ -167,7 +171,7 @@ export const VideoPreviewCard: React.FC<VideoPreviewCardProps> = ({
|
|||||||
borderRadius: 1,
|
borderRadius: 1,
|
||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
}}
|
}}
|
||||||
onClick={handleZoneClick} // Click handler for the zone
|
onClick={handleZoneClick}
|
||||||
onDragOver={handleDragOver}
|
onDragOver={handleDragOver}
|
||||||
onDragLeave={handleDragLeave}
|
onDragLeave={handleDragLeave}
|
||||||
onDrop={handleDrop}
|
onDrop={handleDrop}
|
||||||
@@ -181,8 +185,8 @@ export const VideoPreviewCard: React.FC<VideoPreviewCardProps> = ({
|
|||||||
color="primary"
|
color="primary"
|
||||||
startIcon={<Plus color="white" size={18} />}
|
startIcon={<Plus color="white" size={18} />}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation(); // Prevent `handleZoneClick` from firing
|
e.stopPropagation();
|
||||||
onSelectVideoClick(); // This button triggers the media selection dialog
|
onSelectVideoClick();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Выбрать файл
|
Выбрать файл
|
||||||
@@ -193,7 +197,7 @@ export const VideoPreviewCard: React.FC<VideoPreviewCardProps> = ({
|
|||||||
ref={fileInputRef}
|
ref={fileInputRef}
|
||||||
onChange={handleFileInputChange}
|
onChange={handleFileInputChange}
|
||||||
style={{ display: "none" }}
|
style={{ display: "none" }}
|
||||||
accept="video/*" // Accept only video files
|
accept="video/*"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import { defineConfig } from "vite";
|
import { defineConfig, type UserConfigExport } from "vite";
|
||||||
import react from "@vitejs/plugin-react";
|
import react from "@vitejs/plugin-react";
|
||||||
import tailwindcss from "@tailwindcss/vite";
|
import tailwindcss from "@tailwindcss/vite";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
|
|
||||||
// https://vite.dev/config/
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react(), tailwindcss()],
|
plugins: [
|
||||||
|
react(),
|
||||||
|
tailwindcss(),
|
||||||
|
],
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
"@shared": path.resolve(__dirname, "src/shared"),
|
"@shared": path.resolve(__dirname, "src/shared"),
|
||||||
@@ -16,4 +18,9 @@ export default defineConfig({
|
|||||||
"@app": path.resolve(__dirname, "src/app"),
|
"@app": path.resolve(__dirname, "src/app"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
build: {
|
||||||
|
chunkSizeWarningLimit: 5000,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||