Compare commits
2 Commits
db64beb3ee
...
a357994025
| Author | SHA1 | Date | |
|---|---|---|---|
| a357994025 | |||
| 7382a85082 |
@@ -1,62 +0,0 @@
|
|||||||
# Селектор городов
|
|
||||||
|
|
||||||
## Описание функциональности
|
|
||||||
|
|
||||||
Добавлена функциональность выбора города в админ-панели "Белые ночи":
|
|
||||||
|
|
||||||
### Основные возможности:
|
|
||||||
|
|
||||||
1. **Селектор городов в шапке приложения**
|
|
||||||
|
|
||||||
- Расположен рядом с именем пользователя в верхней части приложения
|
|
||||||
- Показывает список всех доступных городов
|
|
||||||
- Имеет иконку MapPin для лучшего UX
|
|
||||||
|
|
||||||
2. **Сохранение в localStorage**
|
|
||||||
|
|
||||||
- Выбранный город автоматически сохраняется в localStorage
|
|
||||||
- При перезагрузке страницы выбранный город восстанавливается
|
|
||||||
|
|
||||||
3. **Автоматическое использование в формах**
|
|
||||||
- При создании новой станции выбранный город автоматически подставляется
|
|
||||||
- При создании нового перевозчика выбранный город автоматически подставляется
|
|
||||||
- Пользователь может изменить город в форме при необходимости
|
|
||||||
|
|
||||||
### Технические детали:
|
|
||||||
|
|
||||||
#### Новые компоненты и сторы:
|
|
||||||
|
|
||||||
- `SelectedCityStore` - стор для управления выбранным городом
|
|
||||||
- `CitySelector` - компонент селектора городов
|
|
||||||
- `useSelectedCity` - хук для удобного доступа к выбранному городу
|
|
||||||
|
|
||||||
#### Интеграция:
|
|
||||||
|
|
||||||
- Селектор добавлен в `Layout` компонент
|
|
||||||
- Интегрирован в `StationCreatePage` и `CarrierCreatePage`
|
|
||||||
- Использует существующий `CityStore` для получения списка городов
|
|
||||||
|
|
||||||
#### Файлы, которые были изменены:
|
|
||||||
|
|
||||||
- `src/widgets/Layout/index.tsx` - добавлен CitySelector
|
|
||||||
- `src/pages/Station/StationCreatePage/index.tsx` - автоматическая подстановка города
|
|
||||||
- `src/pages/Carrier/CarrierCreatePage/index.tsx` - автоматическая подстановка города
|
|
||||||
- `src/shared/store/index.ts` - добавлен экспорт SelectedCityStore
|
|
||||||
- `src/widgets/index.ts` - добавлен экспорт CitySelector
|
|
||||||
- `src/shared/index.tsx` - добавлен экспорт hooks
|
|
||||||
|
|
||||||
#### Новые файлы:
|
|
||||||
|
|
||||||
- `src/shared/store/SelectedCityStore/index.ts`
|
|
||||||
- `src/widgets/CitySelector/index.tsx`
|
|
||||||
- `src/shared/hooks/useSelectedCity.ts`
|
|
||||||
- `src/shared/hooks/index.ts`
|
|
||||||
|
|
||||||
### Использование:
|
|
||||||
|
|
||||||
1. Пользователь выбирает город в селекторе в шапке приложения
|
|
||||||
2. Выбранный город сохраняется в localStorage
|
|
||||||
3. При создании новой станции или перевозчика выбранный город автоматически подставляется в форму
|
|
||||||
4. Пользователь может изменить город в форме если нужно
|
|
||||||
|
|
||||||
Функциональность полностью интегрирована и готова к использованию.
|
|
||||||
@@ -52,7 +52,7 @@ import { FeatureLike } from "ol/Feature";
|
|||||||
import { createEmpty, extend, getCenter } from "ol/extent";
|
import { createEmpty, extend, getCenter } from "ol/extent";
|
||||||
|
|
||||||
// --- CUSTOM SCROLLBAR STYLES ---
|
// --- CUSTOM SCROLLBAR STYLES ---
|
||||||
const scrollbarHideStyles = `
|
const scrollbarStyles = `
|
||||||
.scrollbar-hide {
|
.scrollbar-hide {
|
||||||
-ms-overflow-style: none;
|
-ms-overflow-style: none;
|
||||||
scrollbar-width: none;
|
scrollbar-width: none;
|
||||||
@@ -60,11 +60,34 @@ const scrollbarHideStyles = `
|
|||||||
.scrollbar-hide::-webkit-scrollbar {
|
.scrollbar-hide::-webkit-scrollbar {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.scrollbar-visible {
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: #cbd5e1 #f1f5f9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scrollbar-visible::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scrollbar-visible::-webkit-scrollbar-track {
|
||||||
|
background: #f1f5f9;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scrollbar-visible::-webkit-scrollbar-thumb {
|
||||||
|
background: #cbd5e1;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scrollbar-visible::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #94a3b8;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
if (typeof document !== "undefined") {
|
if (typeof document !== "undefined") {
|
||||||
const styleElement = document.createElement("style");
|
const styleElement = document.createElement("style");
|
||||||
styleElement.textContent = scrollbarHideStyles;
|
styleElement.textContent = scrollbarStyles;
|
||||||
document.head.appendChild(styleElement);
|
document.head.appendChild(styleElement);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,7 +96,13 @@ if (typeof document !== "undefined") {
|
|||||||
import { languageInstance } from "@shared";
|
import { languageInstance } from "@shared";
|
||||||
import { makeAutoObservable } from "mobx";
|
import { makeAutoObservable } from "mobx";
|
||||||
|
|
||||||
import { stationsStore, routeStore, sightsStore, menuStore } from "@shared";
|
import {
|
||||||
|
stationsStore,
|
||||||
|
routeStore,
|
||||||
|
sightsStore,
|
||||||
|
menuStore,
|
||||||
|
selectedCityStore,
|
||||||
|
} from "@shared";
|
||||||
|
|
||||||
// Функция для сброса кешей карты
|
// Функция для сброса кешей карты
|
||||||
export const clearMapCaches = () => {
|
export const clearMapCaches = () => {
|
||||||
@@ -101,6 +130,7 @@ interface ApiStation {
|
|||||||
name: string;
|
name: string;
|
||||||
latitude: number;
|
latitude: number;
|
||||||
longitude: number;
|
longitude: number;
|
||||||
|
city_id: number;
|
||||||
created_at?: string;
|
created_at?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,12 +140,12 @@ interface ApiSight {
|
|||||||
description: string;
|
description: string;
|
||||||
latitude: number;
|
latitude: number;
|
||||||
longitude: number;
|
longitude: number;
|
||||||
|
city_id: number;
|
||||||
created_at?: string;
|
created_at?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SortType = "name_asc" | "name_desc" | "date_asc" | "date_desc";
|
export type SortType = "name_asc" | "name_desc" | "date_asc" | "date_desc";
|
||||||
|
|
||||||
|
|
||||||
class MapStore {
|
class MapStore {
|
||||||
constructor() {
|
constructor() {
|
||||||
makeAutoObservable(this);
|
makeAutoObservable(this);
|
||||||
@@ -151,16 +181,32 @@ class MapStore {
|
|||||||
return sorted.sort((a, b) => b.name.localeCompare(a.name));
|
return sorted.sort((a, b) => b.name.localeCompare(a.name));
|
||||||
case "date_asc":
|
case "date_asc":
|
||||||
return sorted.sort((a, b) => {
|
return sorted.sort((a, b) => {
|
||||||
if ('created_at' in a && 'created_at' in b && a.created_at && b.created_at) {
|
if (
|
||||||
return new Date(a.created_at).getTime() - new Date(b.created_at).getTime();
|
"created_at" in a &&
|
||||||
|
"created_at" in b &&
|
||||||
|
a.created_at &&
|
||||||
|
b.created_at
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
new Date(a.created_at).getTime() -
|
||||||
|
new Date(b.created_at).getTime()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
// Фоллбэк: сортировка по ID, если дата недоступна
|
// Фоллбэк: сортировка по ID, если дата недоступна
|
||||||
return a.id - b.id;
|
return a.id - b.id;
|
||||||
});
|
});
|
||||||
case "date_desc":
|
case "date_desc":
|
||||||
return sorted.sort((a, b) => {
|
return sorted.sort((a, b) => {
|
||||||
if ('created_at' in a && 'created_at' in b && a.created_at && b.created_at) {
|
if (
|
||||||
return new Date(b.created_at).getTime() - new Date(a.created_at).getTime();
|
"created_at" in a &&
|
||||||
|
"created_at" in b &&
|
||||||
|
a.created_at &&
|
||||||
|
b.created_at
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
new Date(b.created_at).getTime() -
|
||||||
|
new Date(a.created_at).getTime()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
// Фоллбэк: сортировка по ID, если дата недоступна
|
// Фоллбэк: сортировка по ID, если дата недоступна
|
||||||
return b.id - a.id;
|
return b.id - a.id;
|
||||||
@@ -179,6 +225,26 @@ class MapStore {
|
|||||||
return this.sortFeatures(this.sights, this.sightSort);
|
return this.sortFeatures(this.sights, this.sightSort);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ГЕТТЕРЫ ДЛЯ ФИЛЬТРАЦИИ ПО ГОРОДУ
|
||||||
|
get filteredStations(): ApiStation[] {
|
||||||
|
const selectedCityId = selectedCityStore.selectedCityId;
|
||||||
|
if (!selectedCityId) {
|
||||||
|
return this.sortedStations;
|
||||||
|
}
|
||||||
|
return this.sortedStations.filter(
|
||||||
|
(station) => station.city_id === selectedCityId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
get filteredSights(): ApiSight[] {
|
||||||
|
const selectedCityId = selectedCityStore.selectedCityId;
|
||||||
|
if (!selectedCityId) {
|
||||||
|
return this.sortedSights;
|
||||||
|
}
|
||||||
|
return this.sortedSights.filter(
|
||||||
|
(sight) => sight.city_id === selectedCityId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
getRoutes = async () => {
|
getRoutes = async () => {
|
||||||
const response = await languageInstance("ru").get("/route");
|
const response = await languageInstance("ru").get("/route");
|
||||||
@@ -243,7 +309,15 @@ class MapStore {
|
|||||||
address: "",
|
address: "",
|
||||||
system_name: name,
|
system_name: name,
|
||||||
});
|
});
|
||||||
stationsStore.setCreateCommonData({ latitude, longitude, city_id: 1 });
|
const selectedCityId = selectedCityStore.selectedCityId || 1;
|
||||||
|
const selectedCityName =
|
||||||
|
selectedCityStore.selectedCityName || "Неизвестный город";
|
||||||
|
stationsStore.setCreateCommonData({
|
||||||
|
latitude,
|
||||||
|
longitude,
|
||||||
|
city_id: selectedCityId,
|
||||||
|
city: selectedCityName,
|
||||||
|
});
|
||||||
|
|
||||||
await stationsStore.createStation();
|
await stationsStore.createStation();
|
||||||
createdItem =
|
createdItem =
|
||||||
@@ -290,7 +364,11 @@ class MapStore {
|
|||||||
sightsStore.updateCreateSight("en", { name, address: "" });
|
sightsStore.updateCreateSight("en", { name, address: "" });
|
||||||
sightsStore.updateCreateSight("zh", { name, address: "" });
|
sightsStore.updateCreateSight("zh", { name, address: "" });
|
||||||
|
|
||||||
await sightsStore.createSightAction(1, { latitude, longitude });
|
const selectedCityId = selectedCityStore.selectedCityId || 1;
|
||||||
|
await sightsStore.createSightAction(selectedCityId, {
|
||||||
|
latitude,
|
||||||
|
longitude,
|
||||||
|
});
|
||||||
createdItem = sightsStore.sights[sightsStore.sights.length - 1];
|
createdItem = sightsStore.sights[sightsStore.sights.length - 1];
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`Unknown feature type for creation: ${featureType}`);
|
throw new Error(`Unknown feature type for creation: ${featureType}`);
|
||||||
@@ -1181,9 +1259,9 @@ class MapService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public loadFeaturesFromApi(
|
public loadFeaturesFromApi(
|
||||||
apiStations: typeof mapStore.stations,
|
_apiStations: typeof mapStore.stations,
|
||||||
apiRoutes: typeof mapStore.routes,
|
apiRoutes: typeof mapStore.routes,
|
||||||
apiSights: typeof mapStore.sights
|
_apiSights: typeof mapStore.sights
|
||||||
): void {
|
): void {
|
||||||
if (!this.map) return;
|
if (!this.map) return;
|
||||||
|
|
||||||
@@ -1191,7 +1269,11 @@ class MapService {
|
|||||||
const pointFeatures: Feature<Point>[] = [];
|
const pointFeatures: Feature<Point>[] = [];
|
||||||
const lineFeatures: Feature<LineString>[] = [];
|
const lineFeatures: Feature<LineString>[] = [];
|
||||||
|
|
||||||
apiStations.forEach((station) => {
|
// Используем фильтрованные данные из mapStore
|
||||||
|
const filteredStations = mapStore.filteredStations;
|
||||||
|
const filteredSights = mapStore.filteredSights;
|
||||||
|
|
||||||
|
filteredStations.forEach((station) => {
|
||||||
if (station.longitude == null || station.latitude == null) return;
|
if (station.longitude == null || station.latitude == null) return;
|
||||||
const point = new Point(
|
const point = new Point(
|
||||||
transform(
|
transform(
|
||||||
@@ -1206,7 +1288,7 @@ class MapService {
|
|||||||
pointFeatures.push(feature);
|
pointFeatures.push(feature);
|
||||||
});
|
});
|
||||||
|
|
||||||
apiSights.forEach((sight) => {
|
filteredSights.forEach((sight) => {
|
||||||
if (sight.longitude == null || sight.latitude == null) return;
|
if (sight.longitude == null || sight.latitude == null) return;
|
||||||
const point = new Point(
|
const point = new Point(
|
||||||
transform([sight.longitude, sight.latitude], "EPSG:4326", projection)
|
transform([sight.longitude, sight.latitude], "EPSG:4326", projection)
|
||||||
@@ -1329,6 +1411,7 @@ class MapService {
|
|||||||
name: properties.name,
|
name: properties.name,
|
||||||
latitude: lat,
|
latitude: lat,
|
||||||
longitude: lon,
|
longitude: lon,
|
||||||
|
city_id: properties.city_id || 1, // Default city_id if not available
|
||||||
});
|
});
|
||||||
} else if (featureType === "sight") {
|
} else if (featureType === "sight") {
|
||||||
const coords = (geometry as Point).getCoordinates();
|
const coords = (geometry as Point).getCoordinates();
|
||||||
@@ -1339,6 +1422,7 @@ class MapService {
|
|||||||
description: properties.description,
|
description: properties.description,
|
||||||
latitude: lat,
|
latitude: lat,
|
||||||
longitude: lon,
|
longitude: lon,
|
||||||
|
city_id: properties.city_id || 1, // Default city_id if not available
|
||||||
});
|
});
|
||||||
} else if (featureType === "route") {
|
} else if (featureType === "route") {
|
||||||
const coords = (geometry as LineString).getCoordinates();
|
const coords = (geometry as LineString).getCoordinates();
|
||||||
@@ -2188,7 +2272,8 @@ interface MapSightbarProps {
|
|||||||
activeSection: string | null;
|
activeSection: string | null;
|
||||||
setActiveSection: (section: string | null) => void;
|
setActiveSection: (section: string | null) => void;
|
||||||
}
|
}
|
||||||
const MapSightbar: React.FC<MapSightbarProps> = observer(({
|
const MapSightbar: React.FC<MapSightbarProps> = observer(
|
||||||
|
({
|
||||||
mapService,
|
mapService,
|
||||||
mapFeatures,
|
mapFeatures,
|
||||||
selectedFeature,
|
selectedFeature,
|
||||||
@@ -2203,21 +2288,71 @@ const MapSightbar: React.FC<MapSightbarProps> = observer(({
|
|||||||
const [sightSort, setSightSort] = useState<SortType>("name_asc");
|
const [sightSort, setSightSort] = useState<SortType>("name_asc");
|
||||||
|
|
||||||
const { isOpen } = menuStore;
|
const { isOpen } = menuStore;
|
||||||
|
const { selectedCityId } = selectedCityStore;
|
||||||
|
|
||||||
const actualFeatures = useMemo(
|
const actualFeatures = useMemo(
|
||||||
() => mapFeatures.filter((f) => !f.get("isProxy")),
|
() => mapFeatures.filter((f) => !f.get("isProxy")),
|
||||||
[mapFeatures]
|
[mapFeatures]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Создаем объединенный список всех объектов для поиска
|
||||||
|
const allFeatures = useMemo(() => {
|
||||||
|
const stations = mapStore.filteredStations.map((station) => {
|
||||||
|
const feature = new Feature({
|
||||||
|
geometry: new Point(
|
||||||
|
transform(
|
||||||
|
[station.longitude, station.latitude],
|
||||||
|
"EPSG:4326",
|
||||||
|
"EPSG:3857"
|
||||||
|
)
|
||||||
|
),
|
||||||
|
name: station.name,
|
||||||
|
});
|
||||||
|
feature.setId(`station-${station.id}`);
|
||||||
|
feature.set("featureType", "station");
|
||||||
|
feature.set("created_at", station.created_at);
|
||||||
|
return feature;
|
||||||
|
});
|
||||||
|
|
||||||
|
const sights = mapStore.filteredSights.map((sight) => {
|
||||||
|
const feature = new Feature({
|
||||||
|
geometry: new Point(
|
||||||
|
transform(
|
||||||
|
[sight.longitude, sight.latitude],
|
||||||
|
"EPSG:4326",
|
||||||
|
"EPSG:3857"
|
||||||
|
)
|
||||||
|
),
|
||||||
|
name: sight.name,
|
||||||
|
description: sight.description,
|
||||||
|
});
|
||||||
|
feature.setId(`sight-${sight.id}`);
|
||||||
|
feature.set("featureType", "sight");
|
||||||
|
feature.set("created_at", sight.created_at);
|
||||||
|
return feature;
|
||||||
|
});
|
||||||
|
|
||||||
|
const lines = actualFeatures.filter(
|
||||||
|
(f) => f.get("featureType") === "route"
|
||||||
|
);
|
||||||
|
|
||||||
|
return [...stations, ...sights, ...lines];
|
||||||
|
}, [
|
||||||
|
mapStore.filteredStations,
|
||||||
|
mapStore.filteredSights,
|
||||||
|
actualFeatures,
|
||||||
|
selectedCityId,
|
||||||
|
mapStore,
|
||||||
|
]);
|
||||||
|
|
||||||
const filteredFeatures = useMemo(() => {
|
const filteredFeatures = useMemo(() => {
|
||||||
if (!searchQuery.trim()) return actualFeatures;
|
if (!searchQuery.trim()) return allFeatures;
|
||||||
return actualFeatures.filter((f) =>
|
return allFeatures.filter((f) =>
|
||||||
((f.get("name") as string) || "")
|
((f.get("name") as string) || "")
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.includes(searchQuery.toLowerCase())
|
.includes(searchQuery.toLowerCase())
|
||||||
);
|
);
|
||||||
}, [actualFeatures, searchQuery]);
|
}, [allFeatures, searchQuery]);
|
||||||
|
|
||||||
const handleFeatureClick = useCallback(
|
const handleFeatureClick = useCallback(
|
||||||
(id: string | number) => {
|
(id: string | number) => {
|
||||||
@@ -2279,13 +2414,13 @@ const MapSightbar: React.FC<MapSightbarProps> = observer(({
|
|||||||
case "name_asc":
|
case "name_asc":
|
||||||
return sorted.sort((a, b) =>
|
return sorted.sort((a, b) =>
|
||||||
((a.get("name") as string) || "").localeCompare(
|
((a.get("name") as string) || "").localeCompare(
|
||||||
((b.get("name") as string) || "")
|
(b.get("name") as string) || ""
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
case "name_desc":
|
case "name_desc":
|
||||||
return sorted.sort((a, b) =>
|
return sorted.sort((a, b) =>
|
||||||
((b.get("name") as string) || "").localeCompare(
|
((b.get("name") as string) || "").localeCompare(
|
||||||
((a.get("name") as string) || "")
|
(a.get("name") as string) || ""
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
case "date_asc":
|
case "date_asc":
|
||||||
@@ -2316,7 +2451,9 @@ const MapSightbar: React.FC<MapSightbarProps> = observer(({
|
|||||||
const stations = filteredFeatures.filter(
|
const stations = filteredFeatures.filter(
|
||||||
(f) => f.get("featureType") === "station"
|
(f) => f.get("featureType") === "station"
|
||||||
);
|
);
|
||||||
const lines = filteredFeatures.filter((f) => f.get("featureType") === "route");
|
const lines = filteredFeatures.filter(
|
||||||
|
(f) => f.get("featureType") === "route"
|
||||||
|
);
|
||||||
const sights = filteredFeatures.filter(
|
const sights = filteredFeatures.filter(
|
||||||
(f) => f.get("featureType") === "sight"
|
(f) => f.get("featureType") === "sight"
|
||||||
);
|
);
|
||||||
@@ -2379,7 +2516,7 @@ const MapSightbar: React.FC<MapSightbarProps> = observer(({
|
|||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
className={`font-medium whitespace-nowrap overflow-x-auto block
|
className={`font-medium whitespace-nowrap overflow-x-auto block
|
||||||
scrollbar-thin scrollbar-thumb-gray-200 scrollbar-track-transparent`}
|
scrollbar-visible`}
|
||||||
title={fName}
|
title={fName}
|
||||||
>
|
>
|
||||||
{fName}
|
{fName}
|
||||||
@@ -2391,7 +2528,8 @@ const MapSightbar: React.FC<MapSightbarProps> = observer(({
|
|||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
const featureTypeVal = feature.get("featureType");
|
const featureTypeVal = feature.get("featureType");
|
||||||
if (featureTypeVal) handleEditFeature(featureTypeVal, fId);
|
if (featureTypeVal)
|
||||||
|
handleEditFeature(featureTypeVal, fId);
|
||||||
}}
|
}}
|
||||||
className="p-1 rounded-full text-gray-400 hover:text-blue-600 hover:bg-blue-100 transition-colors"
|
className="p-1 rounded-full text-gray-400 hover:text-blue-600 hover:bg-blue-100 transition-colors"
|
||||||
title="Редактировать детали"
|
title="Редактировать детали"
|
||||||
@@ -2427,10 +2565,9 @@ const MapSightbar: React.FC<MapSightbarProps> = observer(({
|
|||||||
title: `Остановки (${sortedStations.length})`,
|
title: `Остановки (${sortedStations.length})`,
|
||||||
icon: <Bus size={20} />,
|
icon: <Bus size={20} />,
|
||||||
count: sortedStations.length,
|
count: sortedStations.length,
|
||||||
content: (
|
sortControl: (
|
||||||
<>
|
<div className="flex items-center space-x-2 p-3 bg-white border-b border-gray-200">
|
||||||
<div className="flex items-center space-x-2 mb-2">
|
<label className="text-sm text-gray-700">Сортировка:</label>
|
||||||
<label>Сортировка:</label>
|
|
||||||
<select
|
<select
|
||||||
value={stationSort}
|
value={stationSort}
|
||||||
onChange={(e) => setStationSort(e.target.value as SortType)}
|
onChange={(e) => setStationSort(e.target.value as SortType)}
|
||||||
@@ -2440,15 +2577,15 @@ const MapSightbar: React.FC<MapSightbarProps> = observer(({
|
|||||||
<option value="name_desc">Имя ↓</option>
|
<option value="name_desc">Имя ↓</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
{renderFeatureList(sortedStations, "station", MapPin)}
|
|
||||||
</>
|
|
||||||
),
|
),
|
||||||
|
content: renderFeatureList(sortedStations, "station", MapPin),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "lines",
|
id: "lines",
|
||||||
title: `Маршруты (${lines.length})`,
|
title: `Маршруты (${lines.length})`,
|
||||||
icon: <RouteIcon size={20} />,
|
icon: <RouteIcon size={20} />,
|
||||||
count: lines.length,
|
count: lines.length,
|
||||||
|
sortControl: null,
|
||||||
content: renderFeatureList(lines, "route", ArrowRightLeft),
|
content: renderFeatureList(lines, "route", ArrowRightLeft),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -2456,10 +2593,9 @@ const MapSightbar: React.FC<MapSightbarProps> = observer(({
|
|||||||
title: `Достопримечательности (${sortedSights.length})`,
|
title: `Достопримечательности (${sortedSights.length})`,
|
||||||
icon: <Landmark size={20} />,
|
icon: <Landmark size={20} />,
|
||||||
count: sortedSights.length,
|
count: sortedSights.length,
|
||||||
content: (
|
sortControl: (
|
||||||
<>
|
<div className="flex items-center space-x-2 p-3 bg-white border-b border-gray-200">
|
||||||
<div className="flex items-center space-x-2 mb-2">
|
<label className="text-sm text-gray-700">Сортировка:</label>
|
||||||
<label>Сортировка:</label>
|
|
||||||
<select
|
<select
|
||||||
value={sightSort}
|
value={sightSort}
|
||||||
onChange={(e) => setSightSort(e.target.value as SortType)}
|
onChange={(e) => setSightSort(e.target.value as SortType)}
|
||||||
@@ -2469,9 +2605,8 @@ const MapSightbar: React.FC<MapSightbarProps> = observer(({
|
|||||||
<option value="name_desc">Имя ↓</option>
|
<option value="name_desc">Имя ↓</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
{renderFeatureList(sortedSights, "sight", Landmark)}
|
|
||||||
</>
|
|
||||||
),
|
),
|
||||||
|
content: renderFeatureList(sortedSights, "sight", Landmark),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -2480,7 +2615,11 @@ const MapSightbar: React.FC<MapSightbarProps> = observer(({
|
|||||||
}, [isOpen]);
|
}, [isOpen]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`${isOpen ? "w-[360px]" : "w-[590px]"} transition-all duration-300 ease-in-out relative bg-gray-50 shadow-lg flex flex-col border-l border-gray-200 h-[90vh]`}>
|
<div
|
||||||
|
className={`${
|
||||||
|
isOpen ? "w-[360px]" : "w-[590px]"
|
||||||
|
} transition-all duration-300 ease-in-out relative bg-gray-50 shadow-lg flex flex-col border-l border-gray-200 h-[90vh]`}
|
||||||
|
>
|
||||||
<div className="p-4 bg-gray-700 text-white">
|
<div className="p-4 bg-gray-700 text-white">
|
||||||
<h2 className="text-lg font-semibold">Панель управления</h2>
|
<h2 className="text-lg font-semibold">Панель управления</h2>
|
||||||
</div>
|
</div>
|
||||||
@@ -2493,7 +2632,7 @@ const MapSightbar: React.FC<MapSightbarProps> = observer(({
|
|||||||
className="w-full px-3 py-2 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
className="w-full px-3 py-2 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 flex flex-col min-h-0 overflow-y-auto">
|
<div className="flex-1 flex flex-col min-h-0 overflow-y-auto scrollbar-visible">
|
||||||
{filteredFeatures.length === 0 && searchQuery ? (
|
{filteredFeatures.length === 0 && searchQuery ? (
|
||||||
<div className="p-4 text-center text-gray-500">
|
<div className="p-4 text-center text-gray-500">
|
||||||
Ничего не найдено.
|
Ничего не найдено.
|
||||||
@@ -2536,14 +2675,19 @@ const MapSightbar: React.FC<MapSightbarProps> = observer(({
|
|||||||
▼
|
▼
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
<div
|
{activeSection === s.id && (
|
||||||
className={`overflow-y-auto scrollbar-hide bg-white ${
|
<>
|
||||||
activeSection === s.id ? "block" : "hidden"
|
{s.sortControl && (
|
||||||
}`}
|
<div className="flex-shrink-0">{s.sortControl}</div>
|
||||||
>
|
)}
|
||||||
<div className="p-3 text-sm text-gray-600">{s.content}</div>
|
<div className="overflow-y-auto scrollbar-visible bg-white flex-1 min-h-0">
|
||||||
|
<div className="p-3 text-sm text-gray-600">
|
||||||
|
{s.content}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
@@ -2561,9 +2705,10 @@ const MapSightbar: React.FC<MapSightbarProps> = observer(({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
}
|
||||||
|
);
|
||||||
// --- MAP PAGE COMPONENT ---
|
// --- MAP PAGE COMPONENT ---
|
||||||
export const MapPage: React.FC = () => {
|
export const MapPage: React.FC = observer(() => {
|
||||||
const mapRef = useRef<HTMLDivElement | null>(null);
|
const mapRef = useRef<HTMLDivElement | null>(null);
|
||||||
const tooltipRef = useRef<HTMLDivElement | null>(null);
|
const tooltipRef = useRef<HTMLDivElement | null>(null);
|
||||||
const [mapServiceInstance, setMapServiceInstance] =
|
const [mapServiceInstance, setMapServiceInstance] =
|
||||||
@@ -2584,6 +2729,8 @@ export const MapPage: React.FC = () => {
|
|||||||
string | null
|
string | null
|
||||||
>(() => getStoredActiveSection() || "layers");
|
>(() => getStoredActiveSection() || "layers");
|
||||||
|
|
||||||
|
const { selectedCityId } = selectedCityStore;
|
||||||
|
|
||||||
const handleFeaturesChange = useCallback(
|
const handleFeaturesChange = useCallback(
|
||||||
(feats: Feature<Geometry>[]) => setMapFeatures([...feats]),
|
(feats: Feature<Geometry>[]) => setMapFeatures([...feats]),
|
||||||
[]
|
[]
|
||||||
@@ -2750,6 +2897,22 @@ export const MapPage: React.FC = () => {
|
|||||||
saveActiveSection(activeSectionFromParent);
|
saveActiveSection(activeSectionFromParent);
|
||||||
}, [activeSectionFromParent]);
|
}, [activeSectionFromParent]);
|
||||||
|
|
||||||
|
// Перезагружаем данные при изменении города
|
||||||
|
useEffect(() => {
|
||||||
|
if (mapServiceInstance && !isDataLoading) {
|
||||||
|
// Очищаем текущие объекты на карте
|
||||||
|
mapServiceInstance.pointSource.clear();
|
||||||
|
mapServiceInstance.lineSource.clear();
|
||||||
|
|
||||||
|
// Загружаем новые данные с фильтрацией по городу
|
||||||
|
mapServiceInstance.loadFeaturesFromApi(
|
||||||
|
mapStore.stations,
|
||||||
|
mapStore.routes,
|
||||||
|
mapStore.sights
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, [selectedCityId, mapServiceInstance, isDataLoading]);
|
||||||
|
|
||||||
const showLoader = isMapLoading || isDataLoading;
|
const showLoader = isMapLoading || isDataLoading;
|
||||||
const showContent = mapServiceInstance && !showLoader && !error;
|
const showContent = mapServiceInstance && !showLoader && !error;
|
||||||
const isAnythingSelected =
|
const isAnythingSelected =
|
||||||
@@ -2866,4 +3029,4 @@ export const MapPage: React.FC = () => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|||||||
@@ -67,7 +67,8 @@ 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[language].name;
|
// Проверяем названия на всех языках
|
||||||
|
const isNameMissing = !createStationData.ru.name || !createStationData.en.name || !createStationData.zh.name;
|
||||||
|
|
||||||
if (isCityMissing || isNameMissing) {
|
if (isCityMissing || isNameMissing) {
|
||||||
setIsSaveWarningOpen(true);
|
setIsSaveWarningOpen(true);
|
||||||
|
|||||||
@@ -67,7 +67,8 @@ export const StationEditPage = observer(() => {
|
|||||||
// ОБНОВЛЕННАЯ ФУНКЦИЯ: Проверка и вызов окна или редактирования
|
// ОБНОВЛЕННАЯ ФУНКЦИЯ: Проверка и вызов окна или редактирования
|
||||||
const handleEdit = async () => {
|
const handleEdit = async () => {
|
||||||
const isCityMissing = !editStationData.common.city_id;
|
const isCityMissing = !editStationData.common.city_id;
|
||||||
const isNameMissing = !editStationData[language].name;
|
// Проверяем названия на всех языках
|
||||||
|
const isNameMissing = !editStationData.ru.name || !editStationData.en.name || !editStationData.zh.name;
|
||||||
|
|
||||||
if (isCityMissing || isNameMissing) {
|
if (isCityMissing || isNameMissing) {
|
||||||
setIsSaveWarningOpen(true);
|
setIsSaveWarningOpen(true);
|
||||||
|
|||||||
@@ -5,9 +5,10 @@ export const SaveWithoutCityAgree = ({ blocker }: { blocker: any }) => {
|
|||||||
<div className="fixed top-0 left-0 w-screen h-screen flex justify-center items-center z-10000 bg-black/30">
|
<div className="fixed top-0 left-0 w-screen h-screen flex justify-center items-center z-10000 bg-black/30">
|
||||||
<div className="bg-white p-4 w-140 rounded-lg flex flex-col gap-4 items-center">
|
<div className="bg-white p-4 w-140 rounded-lg flex flex-col gap-4 items-center">
|
||||||
<p className="text-black w-140 text-center">
|
<p className="text-black w-140 text-center">
|
||||||
Вы не указали город и/или не заполнили названия на всех языках.
|
Вы не указали город и/или не заполнили названия на всех языках
|
||||||
|
(русский, английский, китайский).
|
||||||
<br />
|
<br />
|
||||||
Сохранить достопримечательность без этой информации?
|
Сохранить без этой информации?
|
||||||
</p>
|
</p>
|
||||||
<div className="flex gap-4 justify-center">
|
<div className="flex gap-4 justify-center">
|
||||||
<Button variant="contained" onClick={() => blocker.proceed()}>
|
<Button variant="contained" onClick={() => blocker.proceed()}>
|
||||||
|
|||||||
@@ -119,7 +119,8 @@ export const CreateInformationTab = observer(
|
|||||||
|
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
const isCityMissing = !sight.city_id;
|
const isCityMissing = !sight.city_id;
|
||||||
const isNameMissing = !sight[language].name;
|
// Проверяем названия на всех языках
|
||||||
|
const isNameMissing = !sight.ru.name || !sight.en.name || !sight.zh.name;
|
||||||
|
|
||||||
if (isCityMissing || isNameMissing) {
|
if (isCityMissing || isNameMissing) {
|
||||||
setIsSaveWarningOpen(true);
|
setIsSaveWarningOpen(true);
|
||||||
|
|||||||
@@ -128,7 +128,8 @@ export const InformationTab = observer(
|
|||||||
// ОБНОВЛЕННАЯ ФУНКЦИЯ: Проверка и вызов окна или сохранения
|
// ОБНОВЛЕННАЯ ФУНКЦИЯ: Проверка и вызов окна или сохранения
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
const isCityMissing = !sight.common.city_id;
|
const isCityMissing = !sight.common.city_id;
|
||||||
const isNameMissing = !sight[language].name;
|
// Проверяем названия на всех языках
|
||||||
|
const isNameMissing = !sight.ru.name || !sight.en.name || !sight.zh.name;
|
||||||
|
|
||||||
if (isCityMissing || isNameMissing) {
|
if (isCityMissing || isNameMissing) {
|
||||||
setIsSaveWarningOpen(true);
|
setIsSaveWarningOpen(true);
|
||||||
|
|||||||
Reference in New Issue
Block a user