sight edit update
This commit is contained in:
parent
463c593a0e
commit
9927c0afd6
@ -32,6 +32,8 @@
|
|||||||
"i18next": "^24.2.2",
|
"i18next": "^24.2.2",
|
||||||
"js-cookie": "^3.0.5",
|
"js-cookie": "^3.0.5",
|
||||||
"jwt-decode": "^4.0.0",
|
"jwt-decode": "^4.0.0",
|
||||||
|
"mobx": "^6.13.7",
|
||||||
|
"mobx-react-lite": "^4.1.0",
|
||||||
"react": "19.0.0",
|
"react": "19.0.0",
|
||||||
"react-beautiful-dnd": "^13.1.1",
|
"react-beautiful-dnd": "^13.1.1",
|
||||||
"react-dom": "19.0.0",
|
"react-dom": "19.0.0",
|
||||||
|
@ -1,80 +1,132 @@
|
|||||||
import {DataGrid, type DataGridProps, type GridColumnVisibilityModel} from '@mui/x-data-grid'
|
import {
|
||||||
import {Stack, Button, Typography} from '@mui/material'
|
DataGrid,
|
||||||
import {ExportButton} from '@refinedev/mui'
|
type DataGridProps,
|
||||||
import {useExport} from '@refinedev/core'
|
type GridColumnVisibilityModel,
|
||||||
import React, {useState, useEffect, useMemo} from 'react'
|
} from "@mui/x-data-grid";
|
||||||
import Cookies from 'js-cookie'
|
import { Stack, Button, Typography } from "@mui/material";
|
||||||
|
import { ExportButton } from "@refinedev/mui";
|
||||||
|
import { useExport } from "@refinedev/core";
|
||||||
|
import React, { useState, useEffect, useMemo } from "react";
|
||||||
|
import Cookies from "js-cookie";
|
||||||
|
|
||||||
import {localeText} from '../locales/ru/localeText'
|
import { localeText } from "../locales/ru/localeText";
|
||||||
|
|
||||||
interface CustomDataGridProps extends DataGridProps {
|
interface CustomDataGridProps extends DataGridProps {
|
||||||
hasCoordinates?: boolean
|
hasCoordinates?: boolean;
|
||||||
resource?: string // Add this prop
|
resource?: string; // Add this prop
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEV_FIELDS = ['id', 'code', 'country_code', 'city_id', 'carrier_id', 'main_color', 'left_color', 'right_color', 'logo', 'slogan', 'filename', 'arms', 'thumbnail', 'route_sys_number', 'governor_appeal', 'scale_min', 'scale_max', 'rotate', 'center_latitude', 'center_longitude', 'watermark_lu', 'watermark_rd', 'left_article', 'preview_article', 'offset_x', 'offset_y'] as const
|
const DEV_FIELDS = [
|
||||||
|
"id",
|
||||||
|
"code",
|
||||||
|
"country_code",
|
||||||
|
"city_id",
|
||||||
|
"carrier_id",
|
||||||
|
"main_color",
|
||||||
|
"left_color",
|
||||||
|
"right_color",
|
||||||
|
"logo",
|
||||||
|
"slogan",
|
||||||
|
"filename",
|
||||||
|
"arms",
|
||||||
|
"thumbnail",
|
||||||
|
"route_sys_number",
|
||||||
|
"governor_appeal",
|
||||||
|
"scale_min",
|
||||||
|
"scale_max",
|
||||||
|
"rotate",
|
||||||
|
"center_latitude",
|
||||||
|
"center_longitude",
|
||||||
|
"watermark_lu",
|
||||||
|
"watermark_rd",
|
||||||
|
"left_article",
|
||||||
|
"preview_article",
|
||||||
|
"offset_x",
|
||||||
|
"offset_y",
|
||||||
|
] as const;
|
||||||
|
|
||||||
export const CustomDataGrid = ({hasCoordinates = false, columns = [], resource, ...props}: CustomDataGridProps) => {
|
export const CustomDataGrid = ({
|
||||||
|
hasCoordinates = false,
|
||||||
|
columns = [],
|
||||||
|
resource,
|
||||||
|
...props
|
||||||
|
}: CustomDataGridProps) => {
|
||||||
// const isDev = import.meta.env.DEV
|
// const isDev = import.meta.env.DEV
|
||||||
const { triggerExport, isLoading: exportLoading } = useExport({
|
const { triggerExport, isLoading: exportLoading } = useExport({
|
||||||
resource: resource ?? '',
|
resource: resource ?? "",
|
||||||
// pageSize: 100, #*
|
// pageSize: 100, #*
|
||||||
// maxItemCount: 100, #*
|
// maxItemCount: 100, #*
|
||||||
})
|
});
|
||||||
|
|
||||||
const initialShowCoordinates = Cookies.get('showCoordinates') === 'true'
|
const initialShowCoordinates = Cookies.get("showCoordinates") === "true";
|
||||||
const initialShowDevData = false // Default to false in both prod and dev
|
const initialShowDevData = false; // Default to false in both prod and dev
|
||||||
const [showCoordinates, setShowCoordinates] = useState(initialShowCoordinates)
|
const [showCoordinates, setShowCoordinates] = useState(
|
||||||
const [showDevData, setShowDevData] = useState(Cookies.get('showDevData') === 'true')
|
initialShowCoordinates
|
||||||
|
);
|
||||||
|
const [showDevData, setShowDevData] = useState(
|
||||||
|
Cookies.get("showDevData") === "true"
|
||||||
|
);
|
||||||
|
|
||||||
const availableDevFields = useMemo(() => DEV_FIELDS.filter((field) => columns.some((column) => column.field === field)), [columns])
|
const availableDevFields = useMemo(
|
||||||
|
() =>
|
||||||
|
DEV_FIELDS.filter((field) =>
|
||||||
|
columns.some((column) => column.field === field)
|
||||||
|
),
|
||||||
|
[columns]
|
||||||
|
);
|
||||||
|
|
||||||
const initialVisibilityModel = useMemo(() => {
|
const initialVisibilityModel = useMemo(() => {
|
||||||
const model: GridColumnVisibilityModel = {}
|
const model: GridColumnVisibilityModel = {};
|
||||||
|
|
||||||
availableDevFields.forEach((field) => {
|
availableDevFields.forEach((field) => {
|
||||||
model[field] = initialShowDevData
|
model[field] = initialShowDevData;
|
||||||
})
|
});
|
||||||
|
|
||||||
if (hasCoordinates) {
|
if (hasCoordinates) {
|
||||||
model.latitude = initialShowCoordinates
|
model.latitude = initialShowCoordinates;
|
||||||
model.longitude = initialShowCoordinates
|
model.longitude = initialShowCoordinates;
|
||||||
}
|
}
|
||||||
|
|
||||||
return model
|
return model;
|
||||||
}, [availableDevFields, hasCoordinates, initialShowCoordinates, initialShowDevData])
|
}, [
|
||||||
|
availableDevFields,
|
||||||
|
hasCoordinates,
|
||||||
|
initialShowCoordinates,
|
||||||
|
initialShowDevData,
|
||||||
|
]);
|
||||||
|
|
||||||
const [columnVisibilityModel, setColumnVisibilityModel] = useState<GridColumnVisibilityModel>(initialVisibilityModel)
|
const [columnVisibilityModel, setColumnVisibilityModel] =
|
||||||
|
useState<GridColumnVisibilityModel>(initialVisibilityModel);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setColumnVisibilityModel((prevModel) => {
|
setColumnVisibilityModel((prevModel) => {
|
||||||
const newModel = {...prevModel}
|
const newModel = { ...prevModel };
|
||||||
|
|
||||||
availableDevFields.forEach((field) => {
|
availableDevFields.forEach((field) => {
|
||||||
newModel[field] = showDevData
|
newModel[field] = showDevData;
|
||||||
})
|
});
|
||||||
|
|
||||||
if (hasCoordinates) {
|
if (hasCoordinates) {
|
||||||
newModel.latitude = showCoordinates
|
newModel.latitude = showCoordinates;
|
||||||
newModel.longitude = showCoordinates
|
newModel.longitude = showCoordinates;
|
||||||
}
|
}
|
||||||
|
|
||||||
return newModel
|
return newModel;
|
||||||
})
|
});
|
||||||
|
|
||||||
if (hasCoordinates) {
|
if (hasCoordinates) {
|
||||||
Cookies.set('showCoordinates', String(showCoordinates))
|
Cookies.set("showCoordinates", String(showCoordinates));
|
||||||
}
|
}
|
||||||
Cookies.set('showDevData', String(showDevData))
|
Cookies.set("showDevData", String(showDevData));
|
||||||
}, [showCoordinates, showDevData, hasCoordinates, availableDevFields])
|
}, [showCoordinates, showDevData, hasCoordinates, availableDevFields]);
|
||||||
|
|
||||||
const toggleCoordinates = () => {
|
const toggleCoordinates = () => {
|
||||||
setShowCoordinates((prev) => !prev)
|
setShowCoordinates((prev) => !prev);
|
||||||
}
|
};
|
||||||
|
|
||||||
const toggleDevData = () => {
|
const toggleDevData = () => {
|
||||||
setShowDevData((prev) => !prev)
|
setShowDevData((prev) => !prev);
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack spacing={2}>
|
<Stack spacing={2}>
|
||||||
@ -92,7 +144,7 @@ export const CustomDataGrid = ({hasCoordinates = false, columns = [], resource,
|
|||||||
// paginationModel: {pageSize: 25, page: 0},
|
// paginationModel: {pageSize: 25, page: 0},
|
||||||
// },
|
// },
|
||||||
sorting: {
|
sorting: {
|
||||||
sortModel: [{field: 'id', sort: 'asc'}],
|
sortModel: [{ field: "id", sort: "asc" }],
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
pageSizeOptions={[10, 25, 50, 100]}
|
pageSizeOptions={[10, 25, 50, 100]}
|
||||||
@ -102,21 +154,28 @@ export const CustomDataGrid = ({hasCoordinates = false, columns = [], resource,
|
|||||||
<Stack direction="row" spacing={2} sx={{ mb: 2 }}>
|
<Stack direction="row" spacing={2} sx={{ mb: 2 }}>
|
||||||
{hasCoordinates && (
|
{hasCoordinates && (
|
||||||
<Button variant="contained" onClick={toggleCoordinates}>
|
<Button variant="contained" onClick={toggleCoordinates}>
|
||||||
{showCoordinates ? 'Скрыть координаты' : 'Показать координаты'}
|
{showCoordinates ? "Скрыть координаты" : "Показать координаты"}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{(import.meta.env.DEV || showDevData) && availableDevFields.length > 0 && (
|
{(import.meta.env.DEV || showDevData) &&
|
||||||
|
availableDevFields.length > 0 && (
|
||||||
<Button variant="contained" onClick={toggleDevData}>
|
<Button variant="contained" onClick={toggleDevData}>
|
||||||
{showDevData ? 'Скрыть служебные данные' : 'Показать служебные данные'}
|
{showDevData
|
||||||
|
? "Скрыть служебные данные"
|
||||||
|
: "Показать служебные данные"}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
<ExportButton onClick={triggerExport} loading={exportLoading} hideText={false}>
|
<ExportButton
|
||||||
<Typography sx={{marginLeft: '-2px'}}>Экспорт</Typography>
|
onClick={triggerExport}
|
||||||
|
loading={exportLoading}
|
||||||
|
hideText={false}
|
||||||
|
>
|
||||||
|
<Typography sx={{ marginLeft: "-2px" }}>Экспорт</Typography>
|
||||||
</ExportButton>
|
</ExportButton>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
@ -24,8 +24,9 @@ import {
|
|||||||
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
|
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
|
||||||
import DragIndicatorIcon from "@mui/icons-material/DragIndicator";
|
import DragIndicatorIcon from "@mui/icons-material/DragIndicator";
|
||||||
import { axiosInstance } from "../providers/data";
|
import { axiosInstance } from "../providers/data";
|
||||||
import { TOKEN_KEY } from "../authProvider";
|
|
||||||
import { DragDropContext, Droppable, Draggable } from "@hello-pangea/dnd";
|
import { DragDropContext, Droppable, Draggable } from "@hello-pangea/dnd";
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
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;
|
||||||
@ -79,6 +80,41 @@ export const LinkedItems = <T extends { id: number; [key: string]: any }>({
|
|||||||
type,
|
type,
|
||||||
onSave,
|
onSave,
|
||||||
}: LinkedItemsProps<T>) => {
|
}: LinkedItemsProps<T>) => {
|
||||||
|
const [articleLanguages, setArticleLanguages] = useState<
|
||||||
|
Record<number, string>
|
||||||
|
>({});
|
||||||
|
|
||||||
|
const handleArticleLanguageChange = (
|
||||||
|
articleId: number,
|
||||||
|
languageCode: string
|
||||||
|
) => {
|
||||||
|
setArticleLanguages((prev) => ({ ...prev, [articleId]: languageCode }));
|
||||||
|
console.log(articleId, languageCode);
|
||||||
|
// Отправка запроса на сервер для сохранения языка
|
||||||
|
axios
|
||||||
|
.get(
|
||||||
|
`${import.meta.env.VITE_KRBL_API}/article/${articleId}/`, // Пример эндпоинта
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${localStorage.getItem("refine-auth")}`,
|
||||||
|
"X-language": languageCode.toLowerCase(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.then((response) => {
|
||||||
|
setLinkedItems(
|
||||||
|
linkedItems.map((item) => {
|
||||||
|
if (item.id == articleId) {
|
||||||
|
console.log(response.data);
|
||||||
|
return { ...response.data, language: languageCode };
|
||||||
|
} else {
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const [position, setPosition] = useState<number>(1);
|
const [position, setPosition] = useState<number>(1);
|
||||||
const [items, setItems] = useState<T[]>([]);
|
const [items, setItems] = useState<T[]>([]);
|
||||||
const [linkedItems, setLinkedItems] = useState<T[]>([]);
|
const [linkedItems, setLinkedItems] = useState<T[]>([]);
|
||||||
@ -88,6 +124,33 @@ export const LinkedItems = <T extends { id: number; [key: string]: any }>({
|
|||||||
const [mediaOrder, setMediaOrder] = useState<number>(1);
|
const [mediaOrder, setMediaOrder] = useState<number>(1);
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
|
let availableItems = items.filter(
|
||||||
|
(item) => !linkedItems.some((linked) => linked.id === item.id)
|
||||||
|
);
|
||||||
|
useEffect(() => {
|
||||||
|
if (childResource == "station") {
|
||||||
|
availableItems = availableItems.sort((a, b) =>
|
||||||
|
a.name.localeCompare(b.name)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, [childResource, availableItems]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// При загрузке linkedItems можно запросить текущие языки для статей
|
||||||
|
if (childResource === "article" && linkedItems.length > 0) {
|
||||||
|
const initialLanguages: Record<number, string> = {};
|
||||||
|
linkedItems.forEach((article) => {
|
||||||
|
// Предполагается, что у объекта article есть свойство language
|
||||||
|
if (article.language) {
|
||||||
|
initialLanguages[article.id] = article.language;
|
||||||
|
} else {
|
||||||
|
initialLanguages[article.id] = "RU"; // Или другой язык по умолчанию
|
||||||
|
}
|
||||||
|
});
|
||||||
|
setArticleLanguages(initialLanguages);
|
||||||
|
}
|
||||||
|
}, [linkedItems, childResource]);
|
||||||
|
|
||||||
const onDragEnd = (result: any) => {
|
const onDragEnd = (result: any) => {
|
||||||
if (!result.destination) return;
|
if (!result.destination) return;
|
||||||
|
|
||||||
@ -149,10 +212,6 @@ export const LinkedItems = <T extends { id: number; [key: string]: any }>({
|
|||||||
}
|
}
|
||||||
}, [linkedItems, childResource, parentResource]);
|
}, [linkedItems, childResource, parentResource]);
|
||||||
|
|
||||||
const availableItems = items.filter(
|
|
||||||
(item) => !linkedItems.some((linked) => linked.id === item.id)
|
|
||||||
);
|
|
||||||
|
|
||||||
const linkItem = () => {
|
const linkItem = () => {
|
||||||
if (selectedItemId !== null) {
|
if (selectedItemId !== null) {
|
||||||
const requestData =
|
const requestData =
|
||||||
@ -256,12 +315,19 @@ export const LinkedItems = <T extends { id: number; [key: string]: any }>({
|
|||||||
{field.label}
|
{field.label}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
))}
|
))}
|
||||||
|
{childResource === "article" && (
|
||||||
|
<TableCell key="language">Язык</TableCell>
|
||||||
|
)}
|
||||||
{type === "edit" && (
|
{type === "edit" && (
|
||||||
<TableCell width="120px">Действие</TableCell>
|
<TableCell width="120px">Действие</TableCell>
|
||||||
)}
|
)}
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<Droppable droppableId="droppable">
|
|
||||||
|
<Droppable
|
||||||
|
droppableId="droppable"
|
||||||
|
isDropDisabled={type !== "edit" || !dragAllowed}
|
||||||
|
>
|
||||||
{(provided) => (
|
{(provided) => (
|
||||||
<TableBody
|
<TableBody
|
||||||
ref={provided.innerRef}
|
ref={provided.innerRef}
|
||||||
@ -272,7 +338,7 @@ export const LinkedItems = <T extends { id: number; [key: string]: any }>({
|
|||||||
key={item.id}
|
key={item.id}
|
||||||
draggableId={"q" + String(item.id)}
|
draggableId={"q" + String(item.id)}
|
||||||
index={index}
|
index={index}
|
||||||
isDragDisabled={type !== "edit" && dragAllowed}
|
isDragDisabled={type !== "edit" || !dragAllowed}
|
||||||
>
|
>
|
||||||
{(provided) => (
|
{(provided) => (
|
||||||
<TableRow
|
<TableRow
|
||||||
@ -291,13 +357,105 @@ export const LinkedItems = <T extends { id: number; [key: string]: any }>({
|
|||||||
<TableCell key={String(item.id)}>
|
<TableCell key={String(item.id)}>
|
||||||
{index + 1}
|
{index + 1}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
{fields.map((field) => (
|
{fields.map((field, index) => (
|
||||||
<TableCell key={String(field.data)}>
|
<TableCell
|
||||||
|
key={String(field.data) + String(index)}
|
||||||
|
>
|
||||||
{field.render
|
{field.render
|
||||||
? field.render(item[field.data])
|
? field.render(item[field.data])
|
||||||
: item[field.data]}
|
: item[field.data]}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
))}
|
))}
|
||||||
|
{childResource === "article" && (
|
||||||
|
<TableCell>
|
||||||
|
<Box
|
||||||
|
display="flex"
|
||||||
|
justifyContent="center"
|
||||||
|
alignItems="center"
|
||||||
|
flexDirection="column"
|
||||||
|
gap={1}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
padding: "4px",
|
||||||
|
cursor: "pointer",
|
||||||
|
background:
|
||||||
|
articleLanguages[item.id] === "RU"
|
||||||
|
? theme.palette.primary.main
|
||||||
|
: "transparent",
|
||||||
|
color:
|
||||||
|
articleLanguages[item.id] === "RU"
|
||||||
|
? theme.palette.primary.contrastText
|
||||||
|
: theme.palette.text.primary,
|
||||||
|
border:
|
||||||
|
articleLanguages[item.id] !== "RU"
|
||||||
|
? `1px solid ${theme.palette.primary.main}`
|
||||||
|
: "none",
|
||||||
|
}}
|
||||||
|
onClick={() =>
|
||||||
|
handleArticleLanguageChange(
|
||||||
|
item.id,
|
||||||
|
"RU"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
RU
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
padding: "4px",
|
||||||
|
cursor: "pointer",
|
||||||
|
background:
|
||||||
|
articleLanguages[item.id] === "EN"
|
||||||
|
? theme.palette.primary.main
|
||||||
|
: "transparent",
|
||||||
|
color:
|
||||||
|
articleLanguages[item.id] === "EN"
|
||||||
|
? theme.palette.primary.contrastText
|
||||||
|
: theme.palette.text.primary,
|
||||||
|
border:
|
||||||
|
articleLanguages[item.id] !== "EN"
|
||||||
|
? `1px solid ${theme.palette.primary.main}`
|
||||||
|
: "none",
|
||||||
|
}}
|
||||||
|
onClick={() =>
|
||||||
|
handleArticleLanguageChange(
|
||||||
|
item.id,
|
||||||
|
"EN"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
EN
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
padding: "4px",
|
||||||
|
cursor: "pointer",
|
||||||
|
background:
|
||||||
|
articleLanguages[item.id] === "ZH"
|
||||||
|
? theme.palette.primary.main
|
||||||
|
: "transparent",
|
||||||
|
color:
|
||||||
|
articleLanguages[item.id] === "ZH"
|
||||||
|
? theme.palette.primary.contrastText
|
||||||
|
: theme.palette.text.primary,
|
||||||
|
border:
|
||||||
|
articleLanguages[item.id] !== "ZH"
|
||||||
|
? `1px solid ${theme.palette.primary.main}`
|
||||||
|
: "none",
|
||||||
|
}}
|
||||||
|
onClick={() =>
|
||||||
|
handleArticleLanguageChange(
|
||||||
|
item.id,
|
||||||
|
"ZH"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
ZN
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</TableCell>
|
||||||
|
)}
|
||||||
{type === "edit" && (
|
{type === "edit" && (
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Button
|
<Button
|
||||||
@ -334,7 +492,7 @@ export const LinkedItems = <T extends { id: number; [key: string]: any }>({
|
|||||||
<Autocomplete
|
<Autocomplete
|
||||||
fullWidth
|
fullWidth
|
||||||
value={
|
value={
|
||||||
availableItems.find((item) => item.id === selectedItemId) ||
|
availableItems?.find((item) => item.id === selectedItemId) ||
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
onChange={(_, newValue) =>
|
onChange={(_, newValue) =>
|
||||||
@ -366,6 +524,11 @@ export const LinkedItems = <T extends { id: number; [key: string]: any }>({
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
renderOption={(props, option) => (
|
||||||
|
<li {...props} key={option.id}>
|
||||||
|
{String(option[fields[0].data])}
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{childResource === "article" && (
|
{childResource === "article" && (
|
||||||
|
@ -1,122 +1,181 @@
|
|||||||
import DarkModeOutlined from '@mui/icons-material/DarkModeOutlined'
|
import DarkModeOutlined from "@mui/icons-material/DarkModeOutlined";
|
||||||
import LightModeOutlined from '@mui/icons-material/LightModeOutlined'
|
import LightModeOutlined from "@mui/icons-material/LightModeOutlined";
|
||||||
import AppBar from '@mui/material/AppBar'
|
import AppBar from "@mui/material/AppBar";
|
||||||
import Avatar from '@mui/material/Avatar'
|
import Avatar from "@mui/material/Avatar";
|
||||||
import IconButton from '@mui/material/IconButton'
|
import IconButton from "@mui/material/IconButton";
|
||||||
import Stack from '@mui/material/Stack'
|
import Stack from "@mui/material/Stack";
|
||||||
import Toolbar from '@mui/material/Toolbar'
|
import Toolbar from "@mui/material/Toolbar";
|
||||||
import Typography from '@mui/material/Typography'
|
import Typography from "@mui/material/Typography";
|
||||||
import {useGetIdentity, usePermissions, useWarnAboutChange} from '@refinedev/core'
|
import {
|
||||||
import {HamburgerMenu, RefineThemedLayoutV2HeaderProps} from '@refinedev/mui'
|
useGetIdentity,
|
||||||
import React, {useContext, useEffect} from 'react'
|
useList,
|
||||||
import {ColorModeContext} from '../../contexts/color-mode'
|
usePermissions,
|
||||||
import Cookies from 'js-cookie'
|
useWarnAboutChange,
|
||||||
import {useTranslation} from 'react-i18next'
|
} from "@refinedev/core";
|
||||||
import {Button} from '@mui/material'
|
import { HamburgerMenu, RefineThemedLayoutV2HeaderProps } from "@refinedev/mui";
|
||||||
import {useNavigate} from 'react-router'
|
import React, { useContext, useEffect, useState } from "react";
|
||||||
|
import { ColorModeContext } from "../../contexts/color-mode";
|
||||||
|
import Cookies from "js-cookie";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Select,
|
||||||
|
MenuItem,
|
||||||
|
InputLabel,
|
||||||
|
FormControl,
|
||||||
|
SelectChangeEvent,
|
||||||
|
} from "@mui/material";
|
||||||
|
import { useNavigate } from "react-router";
|
||||||
|
import { cityStore } from "../../store/CityStore";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
type IUser = {
|
type IUser = {
|
||||||
id: number
|
id: number;
|
||||||
name: string
|
name: string;
|
||||||
avatar: string
|
avatar: string;
|
||||||
is_admin: boolean
|
is_admin: boolean;
|
||||||
}
|
};
|
||||||
|
|
||||||
export const Header: React.FC<RefineThemedLayoutV2HeaderProps> = ({sticky = true}) => {
|
export const Header: React.FC<RefineThemedLayoutV2HeaderProps> = observer(
|
||||||
const {mode, setMode} = useContext(ColorModeContext)
|
({ sticky }) => {
|
||||||
const {data: user} = useGetIdentity<IUser>()
|
const { city_id, setCityIdAction } = cityStore;
|
||||||
const {data: permissions} = usePermissions<string[]>()
|
const { data: cities } = useList({
|
||||||
const isAdmin = permissions?.includes('admin')
|
resource: "city",
|
||||||
const {i18n} = useTranslation()
|
});
|
||||||
const {setWarnWhen, warnWhen} = useWarnAboutChange()
|
|
||||||
|
|
||||||
const navigate = useNavigate()
|
const { mode, setMode } = useContext(ColorModeContext);
|
||||||
|
const { data: user } = useGetIdentity<IUser>();
|
||||||
|
const { data: permissions } = usePermissions<string[]>();
|
||||||
|
const isAdmin = permissions?.includes("admin");
|
||||||
|
const { i18n } = useTranslation();
|
||||||
|
const { setWarnWhen, warnWhen } = useWarnAboutChange();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const handleChange = (event: SelectChangeEvent<string>) => {
|
||||||
|
setCityIdAction(event.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
const handleLanguageChange = async (lang: string) => {
|
const handleLanguageChange = async (lang: string) => {
|
||||||
// console.log('Language change requested:', lang)
|
// console.log('Language change requested:', lang)
|
||||||
// console.log('Current warnWhen state:', warnWhen)
|
// console.log('Current warnWhen state:', warnWhen)
|
||||||
|
|
||||||
const form = document.querySelector('form')
|
const form = document.querySelector("form");
|
||||||
const inputs = form?.querySelectorAll<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>('input, textarea, select')
|
const inputs = form?.querySelectorAll<
|
||||||
const saveButton = document.querySelector('.refine-save-button') as HTMLButtonElement
|
HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement
|
||||||
|
>("input, textarea, select");
|
||||||
|
const saveButton = document.querySelector(
|
||||||
|
".refine-save-button"
|
||||||
|
) as HTMLButtonElement;
|
||||||
|
|
||||||
// Сохраняем текущий URL перед любыми действиями
|
// Сохраняем текущий URL перед любыми действиями
|
||||||
const currentLocation = window.location.pathname + window.location.search
|
const currentLocation = window.location.pathname + window.location.search;
|
||||||
|
|
||||||
if (form && saveButton) {
|
if (form && saveButton) {
|
||||||
const hasChanges = Array.from(inputs || []).some((input) => {
|
const hasChanges = Array.from(inputs || []).some((input) => {
|
||||||
if (input instanceof HTMLInputElement || input instanceof HTMLTextAreaElement) {
|
if (
|
||||||
return input.value !== input.defaultValue
|
input instanceof HTMLInputElement ||
|
||||||
|
input instanceof HTMLTextAreaElement
|
||||||
|
) {
|
||||||
|
return input.value !== input.defaultValue;
|
||||||
}
|
}
|
||||||
if (input instanceof HTMLSelectElement) {
|
if (input instanceof HTMLSelectElement) {
|
||||||
return input.value !== input.options[input.selectedIndex].defaultSelected.toString()
|
return (
|
||||||
|
input.value !==
|
||||||
|
input.options[input.selectedIndex].defaultSelected.toString()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return false
|
return false;
|
||||||
})
|
});
|
||||||
|
|
||||||
if (hasChanges || warnWhen) {
|
if (hasChanges || warnWhen) {
|
||||||
try {
|
try {
|
||||||
// console.log('Attempting to save changes...')
|
// console.log('Attempting to save changes...')
|
||||||
setWarnWhen(false)
|
setWarnWhen(false);
|
||||||
saveButton.click()
|
saveButton.click();
|
||||||
// console.log('Save button clicked')
|
// console.log('Save button clicked')
|
||||||
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
|
|
||||||
// После сохранения меняем язык и возвращаемся на ту же страницу
|
// После сохранения меняем язык и возвращаемся на ту же страницу
|
||||||
Cookies.set('lang', lang)
|
Cookies.set("lang", lang);
|
||||||
i18n.changeLanguage(lang)
|
i18n.changeLanguage(lang);
|
||||||
navigate(currentLocation)
|
navigate(currentLocation);
|
||||||
return
|
return;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to save form:', error)
|
console.error("Failed to save form:", error);
|
||||||
setWarnWhen(true)
|
setWarnWhen(true);
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Если нет формы или изменений, просто меняем язык
|
// Если нет формы или изменений, просто меняем язык
|
||||||
// console.log('Setting language cookie:', lang)
|
// console.log('Setting language cookie:', lang)
|
||||||
Cookies.set('lang', lang)
|
Cookies.set("lang", lang);
|
||||||
|
|
||||||
// console.log('Changing i18n language')
|
// console.log('Changing i18n language')
|
||||||
i18n.changeLanguage(lang)
|
i18n.changeLanguage(lang);
|
||||||
|
|
||||||
// Используем текущий URL для навигации
|
// Используем текущий URL для навигации
|
||||||
navigate(0)
|
navigate(0);
|
||||||
}
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const savedLang = Cookies.get('lang') || 'ru'
|
const savedLang = Cookies.get("lang") || "ru";
|
||||||
i18n.changeLanguage(savedLang)
|
i18n.changeLanguage(savedLang);
|
||||||
}, [i18n])
|
}, [i18n]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppBar position={sticky ? 'sticky' : 'relative'}>
|
<AppBar position={sticky ? "sticky" : "relative"}>
|
||||||
<Toolbar>
|
<Toolbar>
|
||||||
<Stack direction="row" width="100%" justifyContent="flex-end" alignItems="center">
|
<Stack
|
||||||
|
direction="row"
|
||||||
|
width="100%"
|
||||||
|
justifyContent="flex-end"
|
||||||
|
alignItems="center"
|
||||||
|
>
|
||||||
<HamburgerMenu />
|
<HamburgerMenu />
|
||||||
<Stack direction="row" width="100%" justifyContent="flex-end" alignItems="center" spacing={2}>
|
|
||||||
|
<Stack
|
||||||
|
direction="row"
|
||||||
|
width="100%"
|
||||||
|
justifyContent="flex-end"
|
||||||
|
alignItems="center"
|
||||||
|
spacing={2}
|
||||||
|
>
|
||||||
|
<FormControl variant="standard" sx={{ width: "min-content" }}>
|
||||||
|
{city_id && cities && (
|
||||||
|
<Select
|
||||||
|
defaultValue={city_id}
|
||||||
|
value={city_id}
|
||||||
|
onChange={handleChange}
|
||||||
|
>
|
||||||
|
{cities.data?.map((city) => (
|
||||||
|
<MenuItem value={String(city.id)} key={city.id}>
|
||||||
|
{city.name}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
)}
|
||||||
|
</FormControl>
|
||||||
<Stack
|
<Stack
|
||||||
direction="row"
|
direction="row"
|
||||||
spacing={1}
|
spacing={1}
|
||||||
sx={{
|
sx={{
|
||||||
backgroundColor: 'background.paper',
|
backgroundColor: "background.paper",
|
||||||
padding: '4px',
|
padding: "4px",
|
||||||
borderRadius: '4px',
|
borderRadius: "4px",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{['ru', 'en', 'zh'].map((lang) => (
|
{["ru", "en", "zh"].map((lang) => (
|
||||||
<Button
|
<Button
|
||||||
key={lang}
|
key={lang}
|
||||||
onClick={() => handleLanguageChange(lang)}
|
onClick={() => handleLanguageChange(lang)}
|
||||||
variant={i18n.language === lang ? 'contained' : 'outlined'}
|
variant={i18n.language === lang ? "contained" : "outlined"}
|
||||||
size="small"
|
size="small"
|
||||||
sx={{
|
sx={{
|
||||||
minWidth: '30px',
|
minWidth: "30px",
|
||||||
padding: '2px 0px',
|
padding: "2px 0px",
|
||||||
textTransform: 'uppercase',
|
textTransform: "uppercase",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{lang}
|
{lang}
|
||||||
@ -127,24 +186,29 @@ export const Header: React.FC<RefineThemedLayoutV2HeaderProps> = ({sticky = true
|
|||||||
<IconButton
|
<IconButton
|
||||||
color="inherit"
|
color="inherit"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setMode()
|
setMode();
|
||||||
}}
|
}}
|
||||||
sx={{
|
sx={{
|
||||||
marginRight: '2px',
|
marginRight: "2px",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{mode === 'dark' ? <LightModeOutlined /> : <DarkModeOutlined />}
|
{mode === "dark" ? <LightModeOutlined /> : <DarkModeOutlined />}
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
|
||||||
{(user?.avatar || user?.name) && (
|
{(user?.avatar || user?.name) && (
|
||||||
<Stack direction="row" gap="16px" alignItems="center" justifyContent="center">
|
<Stack
|
||||||
|
direction="row"
|
||||||
|
gap="16px"
|
||||||
|
alignItems="center"
|
||||||
|
justifyContent="center"
|
||||||
|
>
|
||||||
{user?.name && (
|
{user?.name && (
|
||||||
<Stack direction="column" alignItems="start" gap="0px">
|
<Stack direction="column" alignItems="start" gap="0px">
|
||||||
<Typography
|
<Typography
|
||||||
sx={{
|
sx={{
|
||||||
display: {
|
display: {
|
||||||
xs: 'none',
|
xs: "none",
|
||||||
sm: 'inline-block',
|
sm: "inline-block",
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
variant="subtitle2"
|
variant="subtitle2"
|
||||||
@ -155,18 +219,18 @@ export const Header: React.FC<RefineThemedLayoutV2HeaderProps> = ({sticky = true
|
|||||||
<Typography
|
<Typography
|
||||||
sx={{
|
sx={{
|
||||||
display: {
|
display: {
|
||||||
xs: 'none',
|
xs: "none",
|
||||||
sm: 'inline-block',
|
sm: "inline-block",
|
||||||
},
|
},
|
||||||
backgroundColor: 'primary.main',
|
backgroundColor: "primary.main",
|
||||||
color: 'rgba(255, 255, 255, 0.7)',
|
color: "rgba(255, 255, 255, 0.7)",
|
||||||
padding: '1px 4px',
|
padding: "1px 4px",
|
||||||
borderRadius: 1,
|
borderRadius: 1,
|
||||||
fontSize: '0.6rem',
|
fontSize: "0.6rem",
|
||||||
}}
|
}}
|
||||||
variant="subtitle2"
|
variant="subtitle2"
|
||||||
>
|
>
|
||||||
{isAdmin ? 'Администратор' : 'Пользователь'}
|
{isAdmin ? "Администратор" : "Пользователь"}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Stack>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
@ -177,5 +241,6 @@ export const Header: React.FC<RefineThemedLayoutV2HeaderProps> = ({sticky = true
|
|||||||
</Stack>
|
</Stack>
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
</AppBar>
|
</AppBar>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
);
|
||||||
|
@ -1,16 +1,27 @@
|
|||||||
import {Box, TextField, Typography, Paper} from '@mui/material'
|
import { Box, TextField, Typography, Paper } from "@mui/material";
|
||||||
import {Create} from '@refinedev/mui'
|
import { Create } from "@refinedev/mui";
|
||||||
import {useForm} from '@refinedev/react-hook-form'
|
import { useForm } from "@refinedev/react-hook-form";
|
||||||
import {Controller} from 'react-hook-form'
|
import { Controller } from "react-hook-form";
|
||||||
import React, {useState, useEffect} from 'react'
|
import React, { useState, useEffect } from "react";
|
||||||
import ReactMarkdown from 'react-markdown'
|
import ReactMarkdown from "react-markdown";
|
||||||
|
import Cookies from "js-cookie";
|
||||||
|
import { MarkdownEditor } from "../../components/MarkdownEditor";
|
||||||
|
import "easymde/dist/easymde.min.css";
|
||||||
|
|
||||||
import {MarkdownEditor} from '../../components/MarkdownEditor'
|
const MemoizedSimpleMDE = React.memo(MarkdownEditor);
|
||||||
import 'easymde/dist/easymde.min.css'
|
|
||||||
|
|
||||||
const MemoizedSimpleMDE = React.memo(MarkdownEditor)
|
|
||||||
|
|
||||||
export const ArticleCreate = () => {
|
export const ArticleCreate = () => {
|
||||||
|
const [language, setLanguage] = useState(Cookies.get("lang")!);
|
||||||
|
const [articleData, setArticleData] = useState<{
|
||||||
|
ru: { heading: string; body: string };
|
||||||
|
en: { heading: string; body: string };
|
||||||
|
zh: { heading: string; body: string };
|
||||||
|
}>({
|
||||||
|
ru: { heading: "", body: "" },
|
||||||
|
en: { heading: "", body: "" },
|
||||||
|
zh: { heading: "", body: "" },
|
||||||
|
});
|
||||||
|
|
||||||
const {
|
const {
|
||||||
saveButtonProps,
|
saveButtonProps,
|
||||||
refineCore: { formLoading },
|
refineCore: { formLoading },
|
||||||
@ -18,43 +29,142 @@ export const ArticleCreate = () => {
|
|||||||
control,
|
control,
|
||||||
watch,
|
watch,
|
||||||
formState: { errors },
|
formState: { errors },
|
||||||
|
setValue,
|
||||||
} = useForm({
|
} = useForm({
|
||||||
refineCoreProps: {
|
refineCoreProps: {
|
||||||
resource: 'article/',
|
resource: "article/",
|
||||||
|
meta: {
|
||||||
|
headers: {
|
||||||
|
"Accept-Language": language,
|
||||||
},
|
},
|
||||||
})
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const [preview, setPreview] = useState('')
|
useEffect(() => {
|
||||||
const [headingPreview, setHeadingPreview] = useState('')
|
const lang = Cookies.get("lang")!;
|
||||||
|
Cookies.set("lang", language);
|
||||||
|
return () => {
|
||||||
|
Cookies.set("lang", lang);
|
||||||
|
};
|
||||||
|
}, [language]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setValue(
|
||||||
|
"heading",
|
||||||
|
articleData[language as keyof typeof articleData]?.heading || ""
|
||||||
|
);
|
||||||
|
setValue(
|
||||||
|
"body",
|
||||||
|
articleData[language as keyof typeof articleData]?.body || ""
|
||||||
|
);
|
||||||
|
setPreview(articleData[language as keyof typeof articleData]?.body || "");
|
||||||
|
setHeadingPreview(
|
||||||
|
articleData[language as keyof typeof articleData]?.heading || ""
|
||||||
|
);
|
||||||
|
}, [language, articleData, setValue]);
|
||||||
|
|
||||||
|
const handleLanguageChange = (lang: string) => {
|
||||||
|
setArticleData((prevData) => ({
|
||||||
|
...prevData,
|
||||||
|
[language]: {
|
||||||
|
heading: watch("heading") || "",
|
||||||
|
body: watch("body") || "",
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
setLanguage(lang);
|
||||||
|
Cookies.set("lang", lang);
|
||||||
|
};
|
||||||
|
|
||||||
|
const [preview, setPreview] = useState("");
|
||||||
|
const [headingPreview, setHeadingPreview] = useState("");
|
||||||
|
|
||||||
// Следим за изменениями в полях body и heading
|
// Следим за изменениями в полях body и heading
|
||||||
const bodyContent = watch('body')
|
const bodyContent = watch("body");
|
||||||
const headingContent = watch('heading')
|
const headingContent = watch("heading");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setPreview(bodyContent || '')
|
setPreview(bodyContent || "");
|
||||||
}, [bodyContent])
|
}, [bodyContent]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setHeadingPreview(headingContent || '')
|
setHeadingPreview(headingContent || "");
|
||||||
}, [headingContent])
|
}, [headingContent]);
|
||||||
|
|
||||||
const simpleMDEOptions = React.useMemo(
|
const simpleMDEOptions = React.useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
placeholder: 'Введите контент в формате Markdown...',
|
placeholder: "Введите контент в формате Markdown...",
|
||||||
spellChecker: false,
|
spellChecker: false,
|
||||||
}),
|
}),
|
||||||
[],
|
[]
|
||||||
)
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Create isLoading={formLoading} saveButtonProps={saveButtonProps}>
|
<Create isLoading={formLoading} saveButtonProps={saveButtonProps}>
|
||||||
<Box sx={{display: 'flex', gap: 2}}>
|
<Box sx={{ display: "flex", flex: 1, gap: 2 }}>
|
||||||
{/* Форма создания */}
|
{/* Форма создания */}
|
||||||
<Box component="form" sx={{flex: 1, display: 'flex', flexDirection: 'column'}} autoComplete="off">
|
<Box sx={{ display: "flex", flex: 1, flexDirection: "column", gap: 2 }}>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
flex: 1,
|
||||||
|
display: "flex",
|
||||||
|
gap: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
cursor: "pointer",
|
||||||
|
flex: 1,
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
bgcolor: language === "ru" ? "primary.main" : "transparent",
|
||||||
|
color: language === "ru" ? "white" : "inherit",
|
||||||
|
p: 1,
|
||||||
|
borderRadius: 1,
|
||||||
|
}}
|
||||||
|
onClick={() => handleLanguageChange("ru")}
|
||||||
|
>
|
||||||
|
RU
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
cursor: "pointer",
|
||||||
|
flex: 1,
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
bgcolor: language === "en" ? "primary.main" : "transparent",
|
||||||
|
color: language === "en" ? "white" : "inherit",
|
||||||
|
p: 1,
|
||||||
|
borderRadius: 1,
|
||||||
|
}}
|
||||||
|
onClick={() => handleLanguageChange("en")}
|
||||||
|
>
|
||||||
|
EN
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
cursor: "pointer",
|
||||||
|
flex: 1,
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
bgcolor: language === "zh" ? "primary.main" : "transparent",
|
||||||
|
color: language === "zh" ? "white" : "inherit",
|
||||||
|
p: 1,
|
||||||
|
borderRadius: 1,
|
||||||
|
}}
|
||||||
|
onClick={() => handleLanguageChange("zh")}
|
||||||
|
>
|
||||||
|
ZH
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
component="form"
|
||||||
|
sx={{ flex: 1, display: "flex", flexDirection: "column" }}
|
||||||
|
autoComplete="off"
|
||||||
|
>
|
||||||
<TextField
|
<TextField
|
||||||
{...register('heading', {
|
{...register("heading", {
|
||||||
required: 'Это поле является обязательным',
|
required: "Это поле является обязательным",
|
||||||
})}
|
})}
|
||||||
error={!!(errors as any)?.heading}
|
error={!!(errors as any)?.heading}
|
||||||
helperText={(errors as any)?.heading?.message}
|
helperText={(errors as any)?.heading?.message}
|
||||||
@ -66,7 +176,21 @@ export const ArticleCreate = () => {
|
|||||||
name="heading"
|
name="heading"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Controller control={control} name="body" rules={{required: 'Это поле является обязательным'}} defaultValue="" render={({field: {onChange, value}}) => <MemoizedSimpleMDE value={value} onChange={onChange} options={simpleMDEOptions} className="my-markdown-editor" />} />
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="body"
|
||||||
|
rules={{ required: "Это поле является обязательным" }}
|
||||||
|
defaultValue=""
|
||||||
|
render={({ field: { onChange, value } }) => (
|
||||||
|
<MemoizedSimpleMDE
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
options={simpleMDEOptions}
|
||||||
|
className="my-markdown-editor"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* Блок предпросмотра */}
|
{/* Блок предпросмотра */}
|
||||||
@ -74,14 +198,15 @@ export const ArticleCreate = () => {
|
|||||||
sx={{
|
sx={{
|
||||||
flex: 1,
|
flex: 1,
|
||||||
p: 2,
|
p: 2,
|
||||||
maxHeight: 'calc(100vh - 200px)',
|
maxHeight: "calc(100vh - 200px)",
|
||||||
overflowY: 'auto',
|
overflowY: "auto",
|
||||||
position: 'sticky',
|
position: "sticky",
|
||||||
top: 16,
|
top: 16,
|
||||||
borderRadius: 2,
|
borderRadius: 2,
|
||||||
border: '1px solid',
|
border: "1px solid",
|
||||||
borderColor: 'primary.main',
|
borderColor: "primary.main",
|
||||||
bgcolor: (theme) => (theme.palette.mode === 'dark' ? 'background.paper' : '#fff'),
|
bgcolor: (theme) =>
|
||||||
|
theme.palette.mode === "dark" ? "background.paper" : "#fff",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Typography variant="h6" gutterBottom color="primary">
|
<Typography variant="h6" gutterBottom color="primary">
|
||||||
@ -93,7 +218,8 @@ export const ArticleCreate = () => {
|
|||||||
variant="h4"
|
variant="h4"
|
||||||
gutterBottom
|
gutterBottom
|
||||||
sx={{
|
sx={{
|
||||||
color: (theme) => (theme.palette.mode === 'dark' ? 'grey.300' : 'grey.800'),
|
color: (theme) =>
|
||||||
|
theme.palette.mode === "dark" ? "grey.300" : "grey.800",
|
||||||
mb: 3,
|
mb: 3,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -103,39 +229,41 @@ export const ArticleCreate = () => {
|
|||||||
{/* Markdown контент */}
|
{/* Markdown контент */}
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
'& img': {
|
"& img": {
|
||||||
maxWidth: '100%',
|
maxWidth: "100%",
|
||||||
height: 'auto',
|
height: "auto",
|
||||||
borderRadius: 1,
|
borderRadius: 1,
|
||||||
},
|
},
|
||||||
'& h1, & h2, & h3, & h4, & h5, & h6': {
|
"& h1, & h2, & h3, & h4, & h5, & h6": {
|
||||||
color: 'primary.main',
|
color: "primary.main",
|
||||||
mt: 2,
|
mt: 2,
|
||||||
mb: 1,
|
mb: 1,
|
||||||
},
|
},
|
||||||
'& p': {
|
"& p": {
|
||||||
mb: 2,
|
mb: 2,
|
||||||
color: (theme) => (theme.palette.mode === 'dark' ? 'grey.300' : 'grey.800'),
|
color: (theme) =>
|
||||||
|
theme.palette.mode === "dark" ? "grey.300" : "grey.800",
|
||||||
},
|
},
|
||||||
'& a': {
|
"& a": {
|
||||||
color: 'primary.main',
|
color: "primary.main",
|
||||||
textDecoration: 'none',
|
textDecoration: "none",
|
||||||
'&:hover': {
|
"&:hover": {
|
||||||
textDecoration: 'underline',
|
textDecoration: "underline",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'& blockquote': {
|
"& blockquote": {
|
||||||
borderLeft: '4px solid',
|
borderLeft: "4px solid",
|
||||||
borderColor: 'primary.main',
|
borderColor: "primary.main",
|
||||||
pl: 2,
|
pl: 2,
|
||||||
my: 2,
|
my: 2,
|
||||||
color: 'text.secondary',
|
color: "text.secondary",
|
||||||
},
|
},
|
||||||
'& code': {
|
"& code": {
|
||||||
bgcolor: (theme) => (theme.palette.mode === 'dark' ? 'grey.900' : 'grey.100'),
|
bgcolor: (theme) =>
|
||||||
|
theme.palette.mode === "dark" ? "grey.900" : "grey.100",
|
||||||
p: 0.5,
|
p: 0.5,
|
||||||
borderRadius: 0.5,
|
borderRadius: 0.5,
|
||||||
color: 'primary.main',
|
color: "primary.main",
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -144,5 +272,5 @@ export const ArticleCreate = () => {
|
|||||||
</Paper>
|
</Paper>
|
||||||
</Box>
|
</Box>
|
||||||
</Create>
|
</Create>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
@ -2,7 +2,7 @@ import { Box, TextField, Typography, Paper } from "@mui/material";
|
|||||||
import { Edit } from "@refinedev/mui";
|
import { Edit } from "@refinedev/mui";
|
||||||
import { useForm } from "@refinedev/react-hook-form";
|
import { useForm } from "@refinedev/react-hook-form";
|
||||||
import { Controller } from "react-hook-form";
|
import { Controller } from "react-hook-form";
|
||||||
import { useParams } from "react-router";
|
import { useLocation, useParams } from "react-router";
|
||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import ReactMarkdown from "react-markdown";
|
import ReactMarkdown from "react-markdown";
|
||||||
import { useList } from "@refinedev/core";
|
import { useList } from "@refinedev/core";
|
||||||
@ -12,31 +12,90 @@ import { LinkedItems } from "../../components/LinkedItems";
|
|||||||
import { MediaItem, mediaFields } from "./types";
|
import { MediaItem, mediaFields } from "./types";
|
||||||
import { TOKEN_KEY } from "../../authProvider";
|
import { TOKEN_KEY } from "../../authProvider";
|
||||||
import "easymde/dist/easymde.min.css";
|
import "easymde/dist/easymde.min.css";
|
||||||
|
import Cookies from "js-cookie";
|
||||||
const MemoizedSimpleMDE = React.memo(MarkdownEditor);
|
const MemoizedSimpleMDE = React.memo(MarkdownEditor);
|
||||||
|
|
||||||
export const ArticleEdit = () => {
|
export const ArticleEdit = () => {
|
||||||
|
// const [initialLanguage] = useState(Cookies.get("lang")!);
|
||||||
|
// const { pathname } = useLocation();
|
||||||
|
|
||||||
|
// useEffect(() => {
|
||||||
|
// Cookies.set("lang", initialLanguage);
|
||||||
|
// }, [pathname]);
|
||||||
|
const [language, setLanguage] = useState(Cookies.get("lang")!);
|
||||||
|
const [articleData, setArticleData] = useState<{
|
||||||
|
ru: { heading: string; body: string };
|
||||||
|
en: { heading: string; body: string };
|
||||||
|
zh: { heading: string; body: string };
|
||||||
|
}>({
|
||||||
|
ru: { heading: "", body: "" },
|
||||||
|
en: { heading: "", body: "" },
|
||||||
|
zh: { heading: "", body: "" },
|
||||||
|
});
|
||||||
|
const { id: articleId } = useParams<{ id: string }>();
|
||||||
|
const [preview, setPreview] = useState("");
|
||||||
|
const [headingPreview, setHeadingPreview] = useState("");
|
||||||
|
const simpleMDEOptions = React.useMemo(
|
||||||
|
() => ({
|
||||||
|
placeholder: "Введите контент в формате Markdown...",
|
||||||
|
spellChecker: false,
|
||||||
|
}),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
saveButtonProps,
|
saveButtonProps,
|
||||||
register,
|
register,
|
||||||
control,
|
control,
|
||||||
|
handleSubmit,
|
||||||
watch,
|
watch,
|
||||||
formState: { errors },
|
formState: { errors },
|
||||||
} = useForm();
|
setValue,
|
||||||
|
} = useForm({
|
||||||
const { id: articleId } = useParams<{ id: string }>();
|
refineCoreProps: {
|
||||||
const [preview, setPreview] = useState("");
|
meta: {
|
||||||
const [headingPreview, setHeadingPreview] = useState("");
|
headers: {
|
||||||
|
"Accept-Language": language,
|
||||||
// Получаем привязанные медиа
|
},
|
||||||
const { data: mediaData } = useList<MediaItem>({
|
},
|
||||||
resource: `article/${articleId}/media`,
|
|
||||||
queryOptions: {
|
|
||||||
enabled: !!articleId,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Следим за изменениями в полях body и heading
|
useEffect(() => {
|
||||||
|
const lang = Cookies.get("lang")!;
|
||||||
|
Cookies.set("lang", language);
|
||||||
|
return () => {
|
||||||
|
Cookies.set("lang", lang);
|
||||||
|
};
|
||||||
|
}, [language]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setValue(
|
||||||
|
"heading",
|
||||||
|
articleData[language as keyof typeof articleData]?.heading || ""
|
||||||
|
);
|
||||||
|
setValue(
|
||||||
|
"body",
|
||||||
|
articleData[language as keyof typeof articleData]?.body || ""
|
||||||
|
);
|
||||||
|
setPreview(articleData[language as keyof typeof articleData]?.body || "");
|
||||||
|
setHeadingPreview(
|
||||||
|
articleData[language as keyof typeof articleData]?.heading || ""
|
||||||
|
);
|
||||||
|
}, [language, articleData, setValue]);
|
||||||
|
|
||||||
|
const handleLanguageChange = (lang: string) => {
|
||||||
|
setArticleData((prevData) => ({
|
||||||
|
...prevData,
|
||||||
|
[language]: {
|
||||||
|
heading: watch("heading") || "",
|
||||||
|
body: watch("body") || "",
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
setLanguage(lang);
|
||||||
|
Cookies.set("lang", lang);
|
||||||
|
};
|
||||||
|
|
||||||
const bodyContent = watch("body");
|
const bodyContent = watch("body");
|
||||||
const headingContent = watch("heading");
|
const headingContent = watch("heading");
|
||||||
|
|
||||||
@ -48,18 +107,79 @@ export const ArticleEdit = () => {
|
|||||||
setHeadingPreview(headingContent || "");
|
setHeadingPreview(headingContent || "");
|
||||||
}, [headingContent]);
|
}, [headingContent]);
|
||||||
|
|
||||||
const simpleMDEOptions = React.useMemo(
|
const onSubmit = (data: { heading: string; body: string }) => {
|
||||||
() => ({
|
// Здесь вы будете отправлять данные на сервер,
|
||||||
placeholder: "Введите контент в формате Markdown...",
|
// учитывая текущий язык (language)
|
||||||
spellChecker: false,
|
console.log("Данные для сохранения:", data, language);
|
||||||
}),
|
// ... ваша логика сохранения ...
|
||||||
[]
|
};
|
||||||
);
|
|
||||||
|
const { data: mediaData } = useList<MediaItem>({
|
||||||
|
resource: `article/${articleId}/media`,
|
||||||
|
queryOptions: {
|
||||||
|
enabled: !!articleId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Edit saveButtonProps={saveButtonProps}>
|
<Edit saveButtonProps={saveButtonProps}>
|
||||||
<Box sx={{ display: "flex", gap: 2 }}>
|
<Box sx={{ display: "flex", gap: 2 }}>
|
||||||
{/* Форма редактирования */}
|
{/* Форма редактирования */}
|
||||||
|
{/* Форма создания */}
|
||||||
|
<Box sx={{ display: "flex", flex: 1, flexDirection: "column", gap: 2 }}>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
flex: 1,
|
||||||
|
display: "flex",
|
||||||
|
gap: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
cursor: "pointer",
|
||||||
|
flex: 1,
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
bgcolor: language === "ru" ? "primary.main" : "transparent",
|
||||||
|
color: language === "ru" ? "white" : "inherit",
|
||||||
|
p: 1,
|
||||||
|
borderRadius: 1,
|
||||||
|
}}
|
||||||
|
onClick={() => handleLanguageChange("ru")}
|
||||||
|
>
|
||||||
|
RU
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
cursor: "pointer",
|
||||||
|
flex: 1,
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
bgcolor: language === "en" ? "primary.main" : "transparent",
|
||||||
|
color: language === "en" ? "white" : "inherit",
|
||||||
|
p: 1,
|
||||||
|
borderRadius: 1,
|
||||||
|
}}
|
||||||
|
onClick={() => handleLanguageChange("en")}
|
||||||
|
>
|
||||||
|
EN
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
cursor: "pointer",
|
||||||
|
flex: 1,
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
bgcolor: language === "zh" ? "primary.main" : "transparent",
|
||||||
|
color: language === "zh" ? "white" : "inherit",
|
||||||
|
p: 1,
|
||||||
|
borderRadius: 1,
|
||||||
|
}}
|
||||||
|
onClick={() => handleLanguageChange("zh")}
|
||||||
|
>
|
||||||
|
ZH
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
<Box
|
<Box
|
||||||
component="form"
|
component="form"
|
||||||
sx={{ flex: 1, display: "flex", flexDirection: "column" }}
|
sx={{ flex: 1, display: "flex", flexDirection: "column" }}
|
||||||
@ -105,6 +225,7 @@ export const ArticleEdit = () => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
{/* Блок предпросмотра */}
|
{/* Блок предпросмотра */}
|
||||||
<Paper
|
<Paper
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import {Autocomplete, Box, TextField} from '@mui/material'
|
import { Autocomplete, Box, TextField } from "@mui/material";
|
||||||
import {Edit, useAutocomplete} from '@refinedev/mui'
|
import { Edit, useAutocomplete } from "@refinedev/mui";
|
||||||
import {useForm} from '@refinedev/react-hook-form'
|
import { useForm } from "@refinedev/react-hook-form";
|
||||||
import {Controller} from 'react-hook-form'
|
import { Controller } from "react-hook-form";
|
||||||
|
|
||||||
export const CarrierEdit = () => {
|
export const CarrierEdit = () => {
|
||||||
const {
|
const {
|
||||||
@ -9,62 +9,82 @@ export const CarrierEdit = () => {
|
|||||||
register,
|
register,
|
||||||
control,
|
control,
|
||||||
formState: { errors },
|
formState: { errors },
|
||||||
} = useForm()
|
} = useForm();
|
||||||
|
|
||||||
const { autocompleteProps: cityAutocompleteProps } = useAutocomplete({
|
const { autocompleteProps: cityAutocompleteProps } = useAutocomplete({
|
||||||
resource: 'city',
|
resource: "city",
|
||||||
onSearch: (value) => [
|
onSearch: (value) => [
|
||||||
{
|
{
|
||||||
field: 'name',
|
field: "name",
|
||||||
operator: 'contains',
|
operator: "contains",
|
||||||
value,
|
value,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
})
|
});
|
||||||
|
|
||||||
const { autocompleteProps: mediaAutocompleteProps } = useAutocomplete({
|
const { autocompleteProps: mediaAutocompleteProps } = useAutocomplete({
|
||||||
resource: 'media',
|
resource: "media",
|
||||||
onSearch: (value) => [
|
onSearch: (value) => [
|
||||||
{
|
{
|
||||||
field: 'media_name',
|
field: "media_name",
|
||||||
operator: 'contains',
|
operator: "contains",
|
||||||
value,
|
value,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
})
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Edit saveButtonProps={saveButtonProps}>
|
<Edit saveButtonProps={saveButtonProps}>
|
||||||
<Box component="form" sx={{display: 'flex', flexDirection: 'column'}} autoComplete="off">
|
<Box
|
||||||
|
component="form"
|
||||||
|
sx={{ display: "flex", flexDirection: "column" }}
|
||||||
|
autoComplete="off"
|
||||||
|
>
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="city_id"
|
name="city_id"
|
||||||
rules={{required: 'Это поле является обязательным'}}
|
rules={{ required: "Это поле является обязательным" }}
|
||||||
defaultValue={null}
|
defaultValue={null}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<Autocomplete
|
<Autocomplete
|
||||||
{...cityAutocompleteProps}
|
{...cityAutocompleteProps}
|
||||||
value={cityAutocompleteProps.options.find((option) => option.id === field.value) || null}
|
value={
|
||||||
|
cityAutocompleteProps.options.find(
|
||||||
|
(option) => option.id === field.value
|
||||||
|
) || null
|
||||||
|
}
|
||||||
onChange={(_, value) => {
|
onChange={(_, value) => {
|
||||||
field.onChange(value?.id || '')
|
field.onChange(value?.id || "");
|
||||||
}}
|
}}
|
||||||
getOptionLabel={(item) => {
|
getOptionLabel={(item) => {
|
||||||
return item ? item.name : ''
|
return item ? item.name : "";
|
||||||
}}
|
}}
|
||||||
isOptionEqualToValue={(option, value) => {
|
isOptionEqualToValue={(option, value) => {
|
||||||
return option.id === value?.id
|
return option.id === value?.id;
|
||||||
}}
|
}}
|
||||||
filterOptions={(options, { inputValue }) => {
|
filterOptions={(options, { inputValue }) => {
|
||||||
return options.filter((option) => option.name.toLowerCase().includes(inputValue.toLowerCase()))
|
return options.filter((option) =>
|
||||||
|
option.name.toLowerCase().includes(inputValue.toLowerCase())
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
renderInput={(params) => <TextField {...params} label="Выберите город" margin="normal" variant="outlined" error={!!errors.city_id} helperText={(errors as any)?.city_id?.message} required />}
|
renderInput={(params) => (
|
||||||
|
<TextField
|
||||||
|
{...params}
|
||||||
|
label="Выберите город"
|
||||||
|
margin="normal"
|
||||||
|
variant="outlined"
|
||||||
|
error={!!errors.city_id}
|
||||||
|
helperText={(errors as any)?.city_id?.message}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
{...register('full_name', {
|
{...register("full_name", {
|
||||||
required: 'Это поле является обязательным',
|
required: "Это поле является обязательным",
|
||||||
})}
|
})}
|
||||||
error={!!(errors as any)?.full_name}
|
error={!!(errors as any)?.full_name}
|
||||||
helperText={(errors as any)?.full_name?.message}
|
helperText={(errors as any)?.full_name?.message}
|
||||||
@ -72,13 +92,13 @@ export const CarrierEdit = () => {
|
|||||||
fullWidth
|
fullWidth
|
||||||
InputLabelProps={{ shrink: true }}
|
InputLabelProps={{ shrink: true }}
|
||||||
type="text"
|
type="text"
|
||||||
label={'Полное имя *'}
|
label={"Полное имя *"}
|
||||||
name="full_name"
|
name="full_name"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
{...register('short_name', {
|
{...register("short_name", {
|
||||||
required: 'Это поле является обязательным',
|
required: "Это поле является обязательным",
|
||||||
})}
|
})}
|
||||||
error={!!(errors as any)?.short_name}
|
error={!!(errors as any)?.short_name}
|
||||||
helperText={(errors as any)?.short_name?.message}
|
helperText={(errors as any)?.short_name?.message}
|
||||||
@ -86,12 +106,12 @@ export const CarrierEdit = () => {
|
|||||||
fullWidth
|
fullWidth
|
||||||
InputLabelProps={{ shrink: true }}
|
InputLabelProps={{ shrink: true }}
|
||||||
type="text"
|
type="text"
|
||||||
label={'Короткое имя *'}
|
label={"Короткое имя *"}
|
||||||
name="short_name"
|
name="short_name"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
{...register('main_color', {
|
{...register("main_color", {
|
||||||
// required: 'Это поле является обязательным',
|
// required: 'Это поле является обязательным',
|
||||||
})}
|
})}
|
||||||
error={!!(errors as any)?.main_color}
|
error={!!(errors as any)?.main_color}
|
||||||
@ -100,20 +120,20 @@ export const CarrierEdit = () => {
|
|||||||
fullWidth
|
fullWidth
|
||||||
InputLabelProps={{ shrink: true }}
|
InputLabelProps={{ shrink: true }}
|
||||||
type="color"
|
type="color"
|
||||||
label={'Основной цвет'}
|
label={"Основной цвет"}
|
||||||
name="main_color"
|
name="main_color"
|
||||||
sx={{
|
sx={{
|
||||||
'& input': {
|
"& input": {
|
||||||
height: '50px',
|
height: "50px",
|
||||||
paddingBlock: '14px',
|
paddingBlock: "14px",
|
||||||
paddingInline: '14px',
|
paddingInline: "14px",
|
||||||
cursor: 'pointer',
|
cursor: "pointer",
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
{...register('left_color', {
|
{...register("left_color", {
|
||||||
// required: 'Это поле является обязательным',
|
// required: 'Это поле является обязательным',
|
||||||
})}
|
})}
|
||||||
error={!!(errors as any)?.left_color}
|
error={!!(errors as any)?.left_color}
|
||||||
@ -122,19 +142,19 @@ export const CarrierEdit = () => {
|
|||||||
fullWidth
|
fullWidth
|
||||||
InputLabelProps={{ shrink: true }}
|
InputLabelProps={{ shrink: true }}
|
||||||
type="color"
|
type="color"
|
||||||
label={'Цвет левого виджета'}
|
label={"Цвет левого виджета"}
|
||||||
name="left_color"
|
name="left_color"
|
||||||
sx={{
|
sx={{
|
||||||
'& input': {
|
"& input": {
|
||||||
height: '50px',
|
height: "50px",
|
||||||
paddingBlock: '14px',
|
paddingBlock: "14px",
|
||||||
paddingInline: '14px',
|
paddingInline: "14px",
|
||||||
cursor: 'pointer',
|
cursor: "pointer",
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
{...register('right_color', {
|
{...register("right_color", {
|
||||||
// required: 'Это поле является обязательным',
|
// required: 'Это поле является обязательным',
|
||||||
})}
|
})}
|
||||||
error={!!(errors as any)?.right_color}
|
error={!!(errors as any)?.right_color}
|
||||||
@ -143,20 +163,20 @@ export const CarrierEdit = () => {
|
|||||||
fullWidth
|
fullWidth
|
||||||
InputLabelProps={{ shrink: true }}
|
InputLabelProps={{ shrink: true }}
|
||||||
type="color"
|
type="color"
|
||||||
label={'Цвет правого виджета'}
|
label={"Цвет правого виджета"}
|
||||||
name="right_color"
|
name="right_color"
|
||||||
sx={{
|
sx={{
|
||||||
'& input': {
|
"& input": {
|
||||||
height: '50px',
|
height: "50px",
|
||||||
paddingBlock: '14px',
|
paddingBlock: "14px",
|
||||||
paddingInline: '14px',
|
paddingInline: "14px",
|
||||||
cursor: 'pointer',
|
cursor: "pointer",
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
{...register('slogan', {
|
{...register("slogan", {
|
||||||
// required: 'Это поле является обязательным',
|
// required: 'Это поле является обязательным',
|
||||||
})}
|
})}
|
||||||
error={!!(errors as any)?.slogan}
|
error={!!(errors as any)?.slogan}
|
||||||
@ -165,7 +185,7 @@ export const CarrierEdit = () => {
|
|||||||
fullWidth
|
fullWidth
|
||||||
InputLabelProps={{ shrink: true }}
|
InputLabelProps={{ shrink: true }}
|
||||||
type="text"
|
type="text"
|
||||||
label={'Слоган'}
|
label={"Слоган"}
|
||||||
name="slogan"
|
name="slogan"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@ -177,24 +197,41 @@ export const CarrierEdit = () => {
|
|||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<Autocomplete
|
<Autocomplete
|
||||||
{...mediaAutocompleteProps}
|
{...mediaAutocompleteProps}
|
||||||
value={mediaAutocompleteProps.options.find((option) => option.id === field.value) || null}
|
value={
|
||||||
|
mediaAutocompleteProps.options.find(
|
||||||
|
(option) => option.id === field.value
|
||||||
|
) || null
|
||||||
|
}
|
||||||
onChange={(_, value) => {
|
onChange={(_, value) => {
|
||||||
field.onChange(value?.id || '')
|
field.onChange(value?.id || "");
|
||||||
}}
|
}}
|
||||||
getOptionLabel={(item) => {
|
getOptionLabel={(item) => {
|
||||||
return item ? item.media_name : ''
|
return item ? item.media_name : "";
|
||||||
}}
|
}}
|
||||||
isOptionEqualToValue={(option, value) => {
|
isOptionEqualToValue={(option, value) => {
|
||||||
return option.id === value?.id
|
return option.id === value?.id;
|
||||||
}}
|
}}
|
||||||
filterOptions={(options, { inputValue }) => {
|
filterOptions={(options, { inputValue }) => {
|
||||||
return options.filter((option) => option.media_name.toLowerCase().includes(inputValue.toLowerCase()))
|
return options.filter((option) =>
|
||||||
|
option.media_name
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(inputValue.toLowerCase())
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
renderInput={(params) => <TextField {...params} label="Выберите логотип" margin="normal" variant="outlined" error={!!errors.logo} helperText={(errors as any)?.logo?.message} />}
|
renderInput={(params) => (
|
||||||
|
<TextField
|
||||||
|
{...params}
|
||||||
|
label="Выберите логотип"
|
||||||
|
margin="normal"
|
||||||
|
variant="outlined"
|
||||||
|
error={!!errors.logo}
|
||||||
|
helperText={(errors as any)?.logo?.message}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
</Edit>
|
</Edit>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
@ -1,90 +1,149 @@
|
|||||||
import {type GridColDef} from '@mui/x-data-grid'
|
import { type GridColDef } from "@mui/x-data-grid";
|
||||||
import {CustomDataGrid} from '../../components/CustomDataGrid'
|
import { CustomDataGrid } from "../../components/CustomDataGrid";
|
||||||
import {DeleteButton, EditButton, List, ShowButton, useDataGrid} from '@refinedev/mui'
|
import {
|
||||||
import React from 'react'
|
DeleteButton,
|
||||||
|
EditButton,
|
||||||
|
List,
|
||||||
|
ShowButton,
|
||||||
|
useDataGrid,
|
||||||
|
} from "@refinedev/mui";
|
||||||
|
import React from "react";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
import { cityStore } from "../../store/CityStore";
|
||||||
|
|
||||||
export const CarrierList = () => {
|
export const CarrierList = observer(() => {
|
||||||
const {dataGridProps} = useDataGrid({})
|
const { city_id } = cityStore;
|
||||||
|
const { dataGridProps } = useDataGrid({
|
||||||
|
resource: "carrier",
|
||||||
|
filters: {
|
||||||
|
permanent: [
|
||||||
|
{
|
||||||
|
field: "cityID",
|
||||||
|
operator: "eq",
|
||||||
|
value: city_id,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const columns = React.useMemo<GridColDef[]>(
|
const columns = React.useMemo<GridColDef[]>(
|
||||||
() => [
|
() => [
|
||||||
{
|
{
|
||||||
field: 'id',
|
field: "id",
|
||||||
headerName: 'ID',
|
headerName: "ID",
|
||||||
type: 'number',
|
type: "number",
|
||||||
minWidth: 50,
|
minWidth: 50,
|
||||||
align: 'left',
|
align: "left",
|
||||||
headerAlign: 'left',
|
headerAlign: "left",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'city_id',
|
field: "city_id",
|
||||||
headerName: 'ID Города',
|
headerName: "ID Города",
|
||||||
type: 'number',
|
type: "number",
|
||||||
minWidth: 100,
|
minWidth: 100,
|
||||||
align: 'left',
|
align: "left",
|
||||||
headerAlign: 'left',
|
headerAlign: "left",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'full_name',
|
field: "full_name",
|
||||||
headerName: 'Полное имя',
|
headerName: "Полное имя",
|
||||||
type: 'string',
|
type: "string",
|
||||||
minWidth: 200,
|
minWidth: 200,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'short_name',
|
field: "short_name",
|
||||||
headerName: 'Короткое имя',
|
headerName: "Короткое имя",
|
||||||
type: 'string',
|
type: "string",
|
||||||
minWidth: 125,
|
minWidth: 125,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'city',
|
field: "city",
|
||||||
headerName: 'Город',
|
headerName: "Город",
|
||||||
type: 'string',
|
type: "string",
|
||||||
minWidth: 125,
|
minWidth: 125,
|
||||||
align: 'left',
|
align: "left",
|
||||||
headerAlign: 'left',
|
headerAlign: "left",
|
||||||
flex: 1,
|
flex: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'main_color',
|
field: "main_color",
|
||||||
headerName: 'Основной цвет',
|
headerName: "Основной цвет",
|
||||||
type: 'string',
|
type: "string",
|
||||||
minWidth: 150,
|
minWidth: 150,
|
||||||
renderCell: ({value}) => <div style={{display: 'grid', placeItems: 'center', width: '100%', height: '100%', backgroundColor: `${value}10`, borderRadius: 10}}>{value}</div>,
|
renderCell: ({ value }) => (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "grid",
|
||||||
|
placeItems: "center",
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
backgroundColor: `${value}10`,
|
||||||
|
borderRadius: 10,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{value}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'left_color',
|
field: "left_color",
|
||||||
headerName: 'Цвет левого виджета',
|
headerName: "Цвет левого виджета",
|
||||||
type: 'string',
|
type: "string",
|
||||||
minWidth: 150,
|
minWidth: 150,
|
||||||
renderCell: ({value}) => <div style={{display: 'grid', placeItems: 'center', width: '100%', height: '100%', backgroundColor: `${value}10`, borderRadius: 10}}>{value}</div>,
|
renderCell: ({ value }) => (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "grid",
|
||||||
|
placeItems: "center",
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
backgroundColor: `${value}10`,
|
||||||
|
borderRadius: 10,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{value}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'right_color',
|
field: "right_color",
|
||||||
headerName: 'Цвет правого виджета',
|
headerName: "Цвет правого виджета",
|
||||||
type: 'string',
|
type: "string",
|
||||||
minWidth: 150,
|
minWidth: 150,
|
||||||
renderCell: ({value}) => <div style={{display: 'grid', placeItems: 'center', width: '100%', height: '100%', backgroundColor: `${value}10`, borderRadius: 10}}>{value}</div>,
|
renderCell: ({ value }) => (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "grid",
|
||||||
|
placeItems: "center",
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
backgroundColor: `${value}10`,
|
||||||
|
borderRadius: 10,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{value}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'logo',
|
field: "logo",
|
||||||
headerName: 'Лого',
|
headerName: "Лого",
|
||||||
type: 'string',
|
type: "string",
|
||||||
minWidth: 150,
|
minWidth: 150,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'slogan',
|
field: "slogan",
|
||||||
headerName: 'Слоган',
|
headerName: "Слоган",
|
||||||
type: 'string',
|
type: "string",
|
||||||
minWidth: 150,
|
minWidth: 150,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'actions',
|
field: "actions",
|
||||||
headerName: 'Действия',
|
headerName: "Действия",
|
||||||
minWidth: 120,
|
minWidth: 120,
|
||||||
display: 'flex',
|
display: "flex",
|
||||||
align: 'right',
|
align: "right",
|
||||||
headerAlign: 'center',
|
headerAlign: "center",
|
||||||
sortable: false,
|
sortable: false,
|
||||||
filterable: false,
|
filterable: false,
|
||||||
disableColumnMenu: true,
|
disableColumnMenu: true,
|
||||||
@ -93,18 +152,22 @@ export const CarrierList = () => {
|
|||||||
<>
|
<>
|
||||||
<EditButton hideText recordItemId={row.id} />
|
<EditButton hideText recordItemId={row.id} />
|
||||||
<ShowButton hideText recordItemId={row.id} />
|
<ShowButton hideText recordItemId={row.id} />
|
||||||
<DeleteButton hideText confirmTitle="Вы уверены?" recordItemId={row.id} />
|
<DeleteButton
|
||||||
|
hideText
|
||||||
|
confirmTitle="Вы уверены?"
|
||||||
|
recordItemId={row.id}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[],
|
[]
|
||||||
)
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<List>
|
<List>
|
||||||
<CustomDataGrid {...dataGridProps} columns={columns} />
|
<CustomDataGrid {...dataGridProps} columns={columns} />
|
||||||
</List>
|
</List>
|
||||||
)
|
);
|
||||||
}
|
});
|
||||||
|
@ -1,136 +1,146 @@
|
|||||||
import {type GridColDef} from '@mui/x-data-grid'
|
import { type GridColDef } from "@mui/x-data-grid";
|
||||||
import {CustomDataGrid} from '../../components/CustomDataGrid'
|
import { CustomDataGrid } from "../../components/CustomDataGrid";
|
||||||
import {DeleteButton, EditButton, List, ShowButton, useDataGrid} from '@refinedev/mui'
|
import {
|
||||||
import {Typography} from '@mui/material'
|
DeleteButton,
|
||||||
import React from 'react'
|
EditButton,
|
||||||
|
List,
|
||||||
|
ShowButton,
|
||||||
|
useDataGrid,
|
||||||
|
} from "@refinedev/mui";
|
||||||
|
import { Typography } from "@mui/material";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
import {localeText} from '../../locales/ru/localeText'
|
import { localeText } from "../../locales/ru/localeText";
|
||||||
|
|
||||||
export const RouteList = () => {
|
export const RouteList = () => {
|
||||||
const { dataGridProps } = useDataGrid({
|
const { dataGridProps } = useDataGrid({
|
||||||
resource: 'route/',
|
resource: "route/",
|
||||||
})
|
});
|
||||||
|
|
||||||
const columns = React.useMemo<GridColDef[]>(
|
const columns = React.useMemo<GridColDef[]>(
|
||||||
() => [
|
() => [
|
||||||
{
|
{
|
||||||
field: 'id',
|
field: "id",
|
||||||
headerName: 'ID',
|
headerName: "ID",
|
||||||
type: 'number',
|
type: "number",
|
||||||
minWidth: 70,
|
minWidth: 70,
|
||||||
display: 'flex',
|
display: "flex",
|
||||||
align: 'left',
|
align: "left",
|
||||||
headerAlign: 'left',
|
headerAlign: "left",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'carrier_id',
|
field: "carrier_id",
|
||||||
headerName: 'ID перевозчика',
|
headerName: "ID перевозчика",
|
||||||
type: 'number',
|
type: "number",
|
||||||
minWidth: 150,
|
minWidth: 150,
|
||||||
display: 'flex',
|
display: "flex",
|
||||||
align: 'left',
|
align: "left",
|
||||||
headerAlign: 'left',
|
headerAlign: "left",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'carrier',
|
field: "carrier",
|
||||||
headerName: 'Перевозчик',
|
headerName: "Перевозчик",
|
||||||
type: 'string',
|
type: "string",
|
||||||
minWidth: 150,
|
minWidth: 150,
|
||||||
display: 'flex',
|
display: "flex",
|
||||||
align: 'left',
|
align: "left",
|
||||||
headerAlign: 'left',
|
headerAlign: "left",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'route_number',
|
field: "route_number",
|
||||||
headerName: 'Номер маршрута',
|
headerName: "Номер маршрута",
|
||||||
type: 'string',
|
type: "string",
|
||||||
minWidth: 150,
|
minWidth: 150,
|
||||||
display: 'flex',
|
display: "flex",
|
||||||
align: 'left',
|
align: "left",
|
||||||
headerAlign: 'left',
|
headerAlign: "left",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'route_sys_number',
|
field: "route_sys_number",
|
||||||
headerName: 'Системный номер маршрута',
|
headerName: "Системный номер маршрута",
|
||||||
type: 'string',
|
type: "string",
|
||||||
minWidth: 120,
|
minWidth: 120,
|
||||||
display: 'flex',
|
display: "flex",
|
||||||
align: 'left',
|
align: "left",
|
||||||
headerAlign: 'left',
|
headerAlign: "left",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'governor_appeal',
|
field: "governor_appeal",
|
||||||
headerName: 'Обращение губернатора',
|
headerName: "Обращение губернатора",
|
||||||
type: 'number',
|
type: "number",
|
||||||
minWidth: 120,
|
minWidth: 120,
|
||||||
display: 'flex',
|
display: "flex",
|
||||||
align: 'left',
|
align: "left",
|
||||||
headerAlign: 'left',
|
headerAlign: "left",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'scale_min',
|
field: "scale_min",
|
||||||
headerName: 'Масштаб (мин)',
|
headerName: "Масштаб (мин)",
|
||||||
type: 'number',
|
type: "number",
|
||||||
minWidth: 120,
|
minWidth: 120,
|
||||||
display: 'flex',
|
display: "flex",
|
||||||
align: 'left',
|
align: "left",
|
||||||
headerAlign: 'left',
|
headerAlign: "left",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'scale_max',
|
field: "scale_max",
|
||||||
headerName: 'Масштаб (макс)',
|
headerName: "Масштаб (макс)",
|
||||||
type: 'number',
|
type: "number",
|
||||||
minWidth: 120,
|
minWidth: 120,
|
||||||
display: 'flex',
|
display: "flex",
|
||||||
align: 'left',
|
align: "left",
|
||||||
headerAlign: 'left',
|
headerAlign: "left",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'rotate',
|
field: "rotate",
|
||||||
headerName: 'Поворот',
|
headerName: "Поворот",
|
||||||
type: 'number',
|
type: "number",
|
||||||
minWidth: 120,
|
minWidth: 120,
|
||||||
display: 'flex',
|
display: "flex",
|
||||||
align: 'left',
|
align: "left",
|
||||||
headerAlign: 'left',
|
headerAlign: "left",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'center_latitude',
|
field: "center_latitude",
|
||||||
headerName: 'Центр. широта',
|
headerName: "Центр. широта",
|
||||||
type: 'number',
|
type: "number",
|
||||||
minWidth: 120,
|
minWidth: 120,
|
||||||
display: 'flex',
|
display: "flex",
|
||||||
align: 'left',
|
align: "left",
|
||||||
headerAlign: 'left',
|
headerAlign: "left",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'center_longitude',
|
field: "center_longitude",
|
||||||
headerName: 'Центр. долгота',
|
headerName: "Центр. долгота",
|
||||||
type: 'number',
|
type: "number",
|
||||||
minWidth: 120,
|
minWidth: 120,
|
||||||
display: 'flex',
|
display: "flex",
|
||||||
align: 'left',
|
align: "left",
|
||||||
headerAlign: 'left',
|
headerAlign: "left",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'route_direction',
|
field: "route_direction",
|
||||||
headerName: 'Направление маршрута',
|
headerName: "Направление маршрута",
|
||||||
type: 'boolean',
|
type: "boolean",
|
||||||
display: 'flex',
|
display: "flex",
|
||||||
align: 'left',
|
align: "left",
|
||||||
headerAlign: 'left',
|
headerAlign: "left",
|
||||||
minWidth: 120,
|
minWidth: 120,
|
||||||
flex: 1,
|
flex: 1,
|
||||||
renderCell: ({value}) => <Typography style={{color: value ? '#48989f' : '#7f6b58'}}>{value ? 'прямое' : 'обратное'}</Typography>,
|
renderCell: ({ value }) => (
|
||||||
|
<Typography style={{ color: value ? "#48989f" : "#7f6b58" }}>
|
||||||
|
{value ? "прямое" : "обратное"}
|
||||||
|
</Typography>
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'actions',
|
field: "actions",
|
||||||
headerName: 'Действия',
|
headerName: "Действия",
|
||||||
cellClassName: 'route-actions',
|
cellClassName: "route-actions",
|
||||||
align: 'right',
|
align: "right",
|
||||||
headerAlign: 'center',
|
headerAlign: "center",
|
||||||
minWidth: 120,
|
minWidth: 120,
|
||||||
display: 'flex',
|
display: "flex",
|
||||||
sortable: false,
|
sortable: false,
|
||||||
filterable: false,
|
filterable: false,
|
||||||
disableColumnMenu: true,
|
disableColumnMenu: true,
|
||||||
@ -139,18 +149,27 @@ export const RouteList = () => {
|
|||||||
<>
|
<>
|
||||||
<EditButton hideText recordItemId={row.id} />
|
<EditButton hideText recordItemId={row.id} />
|
||||||
<ShowButton hideText recordItemId={row.id} />
|
<ShowButton hideText recordItemId={row.id} />
|
||||||
<DeleteButton hideText confirmTitle="Вы уверены?" recordItemId={row.id} />
|
<DeleteButton
|
||||||
|
hideText
|
||||||
|
confirmTitle="Вы уверены?"
|
||||||
|
recordItemId={row.id}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[],
|
[]
|
||||||
)
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<List>
|
<List>
|
||||||
<CustomDataGrid {...dataGridProps} columns={columns} localeText={localeText} getRowId={(row: any) => row.id} />
|
<CustomDataGrid
|
||||||
|
{...dataGridProps}
|
||||||
|
columns={columns}
|
||||||
|
localeText={localeText}
|
||||||
|
getRowId={(row: any) => row.id}
|
||||||
|
/>
|
||||||
</List>
|
</List>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
@ -1,35 +1,36 @@
|
|||||||
import {VEHICLE_TYPES} from '../../lib/constants'
|
import { VEHICLE_TYPES } from "../../lib/constants";
|
||||||
|
|
||||||
export type StationItem = {
|
export type StationItem = {
|
||||||
id: number
|
id: number;
|
||||||
name: string
|
name: string;
|
||||||
description: string
|
description: string;
|
||||||
[key: string]: string | number
|
[key: string]: string | number;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type VehicleItem = {
|
export type VehicleItem = {
|
||||||
id: number
|
id: number;
|
||||||
tail_number: number
|
tail_number: number;
|
||||||
type: number
|
type: number;
|
||||||
[key: string]: string | number
|
[key: string]: string | number;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type FieldType<T> = {
|
export type FieldType<T> = {
|
||||||
label: string
|
label: string;
|
||||||
data: keyof T
|
data: keyof T;
|
||||||
render?: (value: any) => React.ReactNode
|
render?: (value: any) => React.ReactNode;
|
||||||
}
|
};
|
||||||
|
|
||||||
export const stationFields: Array<FieldType<StationItem>> = [
|
export const stationFields: Array<FieldType<StationItem>> = [
|
||||||
{label: 'Название', data: 'system_name'},
|
{ label: "Название", data: "name" },
|
||||||
{label: 'Описание', data: 'description'},
|
{ label: "Описание", data: "description" },
|
||||||
]
|
];
|
||||||
|
|
||||||
export const vehicleFields: Array<FieldType<VehicleItem>> = [
|
export const vehicleFields: Array<FieldType<VehicleItem>> = [
|
||||||
{label: 'Бортовой номер', data: 'tail_number'},
|
{ label: "Бортовой номер", data: "tail_number" },
|
||||||
{
|
{
|
||||||
label: 'Тип',
|
label: "Тип",
|
||||||
data: 'type',
|
data: "type",
|
||||||
render: (value: number) => VEHICLE_TYPES.find((type) => type.value === value)?.label || value,
|
render: (value: number) =>
|
||||||
|
VEHICLE_TYPES.find((type) => type.value === value)?.label || value,
|
||||||
},
|
},
|
||||||
]
|
];
|
||||||
|
@ -2,25 +2,45 @@ import { Autocomplete, Box, TextField, Typography, Paper } from "@mui/material";
|
|||||||
import { Create, useAutocomplete } from "@refinedev/mui";
|
import { Create, useAutocomplete } from "@refinedev/mui";
|
||||||
import { useForm } from "@refinedev/react-hook-form";
|
import { useForm } from "@refinedev/react-hook-form";
|
||||||
import { Controller } from "react-hook-form";
|
import { Controller } from "react-hook-form";
|
||||||
import { Link } from "react-router";
|
import { cityStore } from "../../store/CityStore";
|
||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { TOKEN_KEY } from "../../authProvider";
|
import { TOKEN_KEY } from "../../authProvider";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
import Cookies from "js-cookie";
|
||||||
|
import { useLocation } from "react-router";
|
||||||
|
|
||||||
|
export const SightCreate = observer(() => {
|
||||||
|
const [language, setLanguage] = useState(Cookies.get("lang") || "ru");
|
||||||
|
// Состояния для предпросмотра
|
||||||
|
const handleLanguageChange = (lang: string) => {
|
||||||
|
setLanguage(lang);
|
||||||
|
Cookies.set("lang", lang);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const lang = Cookies.get("lang")!;
|
||||||
|
Cookies.set("lang", language);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
Cookies.set("lang", lang);
|
||||||
|
};
|
||||||
|
}, [language]);
|
||||||
|
|
||||||
export const SightCreate = () => {
|
|
||||||
const {
|
const {
|
||||||
saveButtonProps,
|
saveButtonProps,
|
||||||
refineCore: { formLoading },
|
refineCore: { formLoading },
|
||||||
register,
|
register,
|
||||||
control,
|
control,
|
||||||
watch,
|
watch,
|
||||||
|
setValue,
|
||||||
formState: { errors },
|
formState: { errors },
|
||||||
} = useForm({
|
} = useForm({
|
||||||
refineCoreProps: {
|
refineCoreProps: {
|
||||||
resource: "sight/",
|
resource: "sight/",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
const { city_id } = cityStore;
|
||||||
|
|
||||||
// Состояния для предпросмотра
|
|
||||||
const [namePreview, setNamePreview] = useState("");
|
const [namePreview, setNamePreview] = useState("");
|
||||||
const [coordinatesPreview, setCoordinatesPreview] = useState({
|
const [coordinatesPreview, setCoordinatesPreview] = useState({
|
||||||
latitude: "",
|
latitude: "",
|
||||||
@ -37,6 +57,16 @@ export const SightCreate = () => {
|
|||||||
const [leftArticlePreview, setLeftArticlePreview] = useState("");
|
const [leftArticlePreview, setLeftArticlePreview] = useState("");
|
||||||
const [previewArticlePreview, setPreviewArticlePreview] = useState("");
|
const [previewArticlePreview, setPreviewArticlePreview] = useState("");
|
||||||
|
|
||||||
|
const handleCoordinatesChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const [lat, lon] = e.target.value.split(",").map((s) => s.trim());
|
||||||
|
setCoordinatesPreview({
|
||||||
|
latitude: lat,
|
||||||
|
longitude: lon,
|
||||||
|
});
|
||||||
|
setValue("latitude", lat);
|
||||||
|
setValue("longitude", lon);
|
||||||
|
};
|
||||||
|
|
||||||
// Автокомплиты
|
// Автокомплиты
|
||||||
const { autocompleteProps: cityAutocompleteProps } = useAutocomplete({
|
const { autocompleteProps: cityAutocompleteProps } = useAutocomplete({
|
||||||
resource: "city",
|
resource: "city",
|
||||||
@ -47,6 +77,11 @@ export const SightCreate = () => {
|
|||||||
value,
|
value,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
meta: {
|
||||||
|
headers: {
|
||||||
|
"Accept-Language": language,
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { autocompleteProps: mediaAutocompleteProps } = useAutocomplete({
|
const { autocompleteProps: mediaAutocompleteProps } = useAutocomplete({
|
||||||
@ -73,6 +108,7 @@ export const SightCreate = () => {
|
|||||||
|
|
||||||
// Следим за изменениями во всех полях
|
// Следим за изменениями во всех полях
|
||||||
const nameContent = watch("name");
|
const nameContent = watch("name");
|
||||||
|
const addressContent = watch("address");
|
||||||
const latitudeContent = watch("latitude");
|
const latitudeContent = watch("latitude");
|
||||||
const longitudeContent = watch("longitude");
|
const longitudeContent = watch("longitude");
|
||||||
const cityContent = watch("city_id");
|
const cityContent = watch("city_id");
|
||||||
@ -114,6 +150,12 @@ export const SightCreate = () => {
|
|||||||
);
|
);
|
||||||
}, [thumbnailContent, mediaAutocompleteProps.options]);
|
}, [thumbnailContent, mediaAutocompleteProps.options]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (city_id) {
|
||||||
|
setValue("city_id", +city_id);
|
||||||
|
}
|
||||||
|
}, [city_id, setValue]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const selectedWatermarkLU = mediaAutocompleteProps.options.find(
|
const selectedWatermarkLU = mediaAutocompleteProps.options.find(
|
||||||
(option) => option.id === watermarkLUContent
|
(option) => option.id === watermarkLUContent
|
||||||
@ -157,7 +199,62 @@ export const SightCreate = () => {
|
|||||||
return (
|
return (
|
||||||
<Create isLoading={formLoading} saveButtonProps={saveButtonProps}>
|
<Create isLoading={formLoading} saveButtonProps={saveButtonProps}>
|
||||||
<Box sx={{ display: "flex", gap: 2 }}>
|
<Box sx={{ display: "flex", gap: 2 }}>
|
||||||
|
<Box sx={{ display: "flex", flexDirection: "column", flex: 1, gap: 2 }}>
|
||||||
{/* Форма создания */}
|
{/* Форма создания */}
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
flex: 1,
|
||||||
|
display: "flex",
|
||||||
|
gap: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
cursor: "pointer",
|
||||||
|
flex: 1,
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
bgcolor: language === "ru" ? "primary.main" : "transparent",
|
||||||
|
color: language === "ru" ? "white" : "inherit",
|
||||||
|
borderRadius: 1,
|
||||||
|
p: 1,
|
||||||
|
}}
|
||||||
|
onClick={() => handleLanguageChange("ru")}
|
||||||
|
>
|
||||||
|
RU
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
cursor: "pointer",
|
||||||
|
flex: 1,
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
bgcolor: language === "en" ? "primary.main" : "transparent",
|
||||||
|
color: language === "en" ? "white" : "inherit",
|
||||||
|
borderRadius: 1,
|
||||||
|
p: 1,
|
||||||
|
}}
|
||||||
|
onClick={() => handleLanguageChange("en")}
|
||||||
|
>
|
||||||
|
EN
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
cursor: "pointer",
|
||||||
|
flex: 1,
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
bgcolor: language === "zh" ? "primary.main" : "transparent",
|
||||||
|
color: language === "zh" ? "white" : "inherit",
|
||||||
|
borderRadius: 1,
|
||||||
|
p: 1,
|
||||||
|
}}
|
||||||
|
onClick={() => handleLanguageChange("zh")}
|
||||||
|
>
|
||||||
|
ZH
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
<Box
|
<Box
|
||||||
component="form"
|
component="form"
|
||||||
sx={{ flex: 1, display: "flex", flexDirection: "column" }}
|
sx={{ flex: 1, display: "flex", flexDirection: "column" }}
|
||||||
@ -176,40 +273,53 @@ export const SightCreate = () => {
|
|||||||
label={"Название *"}
|
label={"Название *"}
|
||||||
name="name"
|
name="name"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
{...register("latitude", {
|
value={`${coordinatesPreview.latitude}, ${coordinatesPreview.longitude}`}
|
||||||
required: "Это поле является обязательным",
|
onChange={handleCoordinatesChange}
|
||||||
valueAsNumber: true,
|
|
||||||
})}
|
|
||||||
error={!!(errors as any)?.latitude}
|
error={!!(errors as any)?.latitude}
|
||||||
helperText={(errors as any)?.latitude?.message}
|
helperText={(errors as any)?.latitude?.message}
|
||||||
margin="normal"
|
margin="normal"
|
||||||
fullWidth
|
fullWidth
|
||||||
InputLabelProps={{ shrink: true }}
|
InputLabelProps={{ shrink: true }}
|
||||||
type="number"
|
type="text"
|
||||||
label={"Широта *"}
|
label={"Координаты *"}
|
||||||
name="latitude"
|
|
||||||
/>
|
/>
|
||||||
<TextField
|
<input
|
||||||
|
type="hidden"
|
||||||
{...register("longitude", {
|
{...register("longitude", {
|
||||||
|
value: coordinatesPreview.longitude,
|
||||||
required: "Это поле является обязательным",
|
required: "Это поле является обязательным",
|
||||||
valueAsNumber: true,
|
valueAsNumber: true,
|
||||||
})}
|
})}
|
||||||
error={!!(errors as any)?.longitude}
|
/>
|
||||||
helperText={(errors as any)?.longitude?.message}
|
<input
|
||||||
|
type="hidden"
|
||||||
|
{...register("latitude", {
|
||||||
|
value: coordinatesPreview.latitude,
|
||||||
|
required: "Это поле является обязательным",
|
||||||
|
valueAsNumber: true,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
{...register("address", {
|
||||||
|
required: "Это поле является обязательным",
|
||||||
|
})}
|
||||||
|
error={!!(errors as any)?.address}
|
||||||
|
helperText={(errors as any)?.address?.message}
|
||||||
margin="normal"
|
margin="normal"
|
||||||
fullWidth
|
fullWidth
|
||||||
InputLabelProps={{ shrink: true }}
|
InputLabelProps={{ shrink: true }}
|
||||||
type="number"
|
type="text"
|
||||||
label={"Долгота *"}
|
label={"Адрес *"}
|
||||||
name="longitude"
|
name="address"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="city_id"
|
name="city_id"
|
||||||
rules={{ required: "Это поле является обязательным" }}
|
rules={{ required: "Это поле является обязательным" }}
|
||||||
defaultValue={null}
|
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<Autocomplete
|
<Autocomplete
|
||||||
{...cityAutocompleteProps}
|
{...cityAutocompleteProps}
|
||||||
@ -229,7 +339,9 @@ export const SightCreate = () => {
|
|||||||
}}
|
}}
|
||||||
filterOptions={(options, { inputValue }) => {
|
filterOptions={(options, { inputValue }) => {
|
||||||
return options.filter((option) =>
|
return options.filter((option) =>
|
||||||
option.name.toLowerCase().includes(inputValue.toLowerCase())
|
option.name
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(inputValue.toLowerCase())
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
renderInput={(params) => (
|
renderInput={(params) => (
|
||||||
@ -281,8 +393,8 @@ export const SightCreate = () => {
|
|||||||
label="Выберите обложку"
|
label="Выберите обложку"
|
||||||
margin="normal"
|
margin="normal"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
error={!!errors.arms}
|
error={!!errors.thumbnail}
|
||||||
helperText={(errors as any)?.arms?.message}
|
helperText={(errors as any)?.thumbnail?.message}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@ -324,9 +436,8 @@ export const SightCreate = () => {
|
|||||||
label="Выберите водный знак (Левый верх)"
|
label="Выберите водный знак (Левый верх)"
|
||||||
margin="normal"
|
margin="normal"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
error={!!errors.arms}
|
error={!!errors.watermark_lu}
|
||||||
helperText={(errors as any)?.arms?.message}
|
helperText={(errors as any)?.watermark_lu?.message}
|
||||||
required
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@ -364,12 +475,11 @@ export const SightCreate = () => {
|
|||||||
renderInput={(params) => (
|
renderInput={(params) => (
|
||||||
<TextField
|
<TextField
|
||||||
{...params}
|
{...params}
|
||||||
label="Выберите водный знак (Правый низ)"
|
label="Выберите водный знак (Правый верх)"
|
||||||
margin="normal"
|
margin="normal"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
error={!!errors.arms}
|
error={!!errors.watermark_rd}
|
||||||
helperText={(errors as any)?.arms?.message}
|
helperText={(errors as any)?.watermark_rd?.message}
|
||||||
required
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@ -410,9 +520,8 @@ export const SightCreate = () => {
|
|||||||
label="Левая статья"
|
label="Левая статья"
|
||||||
margin="normal"
|
margin="normal"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
error={!!errors.arms}
|
error={!!errors.left_article}
|
||||||
helperText={(errors as any)?.arms?.message}
|
helperText={(errors as any)?.left_article?.message}
|
||||||
required
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@ -453,17 +562,17 @@ export const SightCreate = () => {
|
|||||||
label="Cтатья-предпросмотр"
|
label="Cтатья-предпросмотр"
|
||||||
margin="normal"
|
margin="normal"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
error={!!errors.arms}
|
error={!!errors.preview_article}
|
||||||
helperText={(errors as any)?.arms?.message}
|
helperText={(errors as any)?.preview_article?.message}
|
||||||
required
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
{/* Блок предпросмотра */}
|
{/* Preview Panel */}
|
||||||
<Paper
|
<Paper
|
||||||
sx={{
|
sx={{
|
||||||
flex: 1,
|
flex: 1,
|
||||||
@ -483,13 +592,14 @@ export const SightCreate = () => {
|
|||||||
Предпросмотр
|
Предпросмотр
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
{/* Название */}
|
{/* Название достопримечательности */}
|
||||||
<Typography
|
<Typography
|
||||||
variant="h4"
|
variant="h4"
|
||||||
gutterBottom
|
gutterBottom
|
||||||
sx={{
|
sx={{
|
||||||
color: (theme) =>
|
color: (theme) =>
|
||||||
theme.palette.mode === "dark" ? "grey.300" : "grey.800",
|
theme.palette.mode === "dark" ? "grey.300" : "grey.800",
|
||||||
|
mb: 3,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{namePreview}
|
{namePreview}
|
||||||
@ -511,6 +621,22 @@ export const SightCreate = () => {
|
|||||||
</Box>
|
</Box>
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
|
{/* Адрес */}
|
||||||
|
<Typography variant="body1" sx={{ mb: 2 }}>
|
||||||
|
<Box component="span" sx={{ color: "text.secondary" }}>
|
||||||
|
Адрес:{" "}
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
component="span"
|
||||||
|
sx={{
|
||||||
|
color: (theme) =>
|
||||||
|
theme.palette.mode === "dark" ? "grey.300" : "grey.800",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{addressContent}
|
||||||
|
</Box>
|
||||||
|
</Typography>
|
||||||
|
|
||||||
{/* Координаты */}
|
{/* Координаты */}
|
||||||
<Typography variant="body1" sx={{ mb: 2 }}>
|
<Typography variant="body1" sx={{ mb: 2 }}>
|
||||||
<Box component="span" sx={{ color: "text.secondary" }}>
|
<Box component="span" sx={{ color: "text.secondary" }}>
|
||||||
@ -523,7 +649,7 @@ export const SightCreate = () => {
|
|||||||
theme.palette.mode === "dark" ? "grey.300" : "grey.800",
|
theme.palette.mode === "dark" ? "grey.300" : "grey.800",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{coordinatesPreview.latitude}, {coordinatesPreview.longitude}
|
{`${coordinatesPreview.latitude}, ${coordinatesPreview.longitude}`}
|
||||||
</Box>
|
</Box>
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
@ -543,8 +669,8 @@ export const SightCreate = () => {
|
|||||||
alt="Обложка"
|
alt="Обложка"
|
||||||
sx={{
|
sx={{
|
||||||
maxWidth: "100%",
|
maxWidth: "100%",
|
||||||
height: "auto",
|
height: "40vh",
|
||||||
borderRadius: 1,
|
borderRadius: 2,
|
||||||
border: "1px solid",
|
border: "1px solid",
|
||||||
borderColor: "primary.main",
|
borderColor: "primary.main",
|
||||||
}}
|
}}
|
||||||
@ -593,7 +719,7 @@ export const SightCreate = () => {
|
|||||||
gutterBottom
|
gutterBottom
|
||||||
sx={{ color: "text.secondary" }}
|
sx={{ color: "text.secondary" }}
|
||||||
>
|
>
|
||||||
Правый нижний:
|
Правый верхний:
|
||||||
</Typography>
|
</Typography>
|
||||||
<Box
|
<Box
|
||||||
component="img"
|
component="img"
|
||||||
@ -615,28 +741,16 @@ export const SightCreate = () => {
|
|||||||
|
|
||||||
{/* Связанные статьи */}
|
{/* Связанные статьи */}
|
||||||
<Box>
|
<Box>
|
||||||
<Typography
|
|
||||||
variant="body1"
|
|
||||||
gutterBottom
|
|
||||||
sx={{ color: "text.secondary" }}
|
|
||||||
>
|
|
||||||
Связанные статьи:
|
|
||||||
</Typography>
|
|
||||||
{leftArticlePreview && (
|
{leftArticlePreview && (
|
||||||
<Typography variant="body1" gutterBottom>
|
<Typography variant="body1" gutterBottom>
|
||||||
<Box component="span" sx={{ color: "text.secondary" }}>
|
<Box component="span" sx={{ color: "text.secondary" }}>
|
||||||
Левая статья:{" "}
|
Левая статья:{" "}
|
||||||
</Box>
|
</Box>
|
||||||
<Box
|
<Box
|
||||||
component={Link}
|
component="span"
|
||||||
to={`/article/show/${watch("left_article")}`}
|
|
||||||
sx={{
|
sx={{
|
||||||
color: (theme) =>
|
color: (theme) =>
|
||||||
theme.palette.mode === "dark" ? "grey.300" : "grey.800",
|
theme.palette.mode === "dark" ? "grey.300" : "grey.800",
|
||||||
textDecoration: "none",
|
|
||||||
"&:hover": {
|
|
||||||
textDecoration: "underline",
|
|
||||||
},
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{leftArticlePreview}
|
{leftArticlePreview}
|
||||||
@ -649,15 +763,10 @@ export const SightCreate = () => {
|
|||||||
Статья-предпросмотр:{" "}
|
Статья-предпросмотр:{" "}
|
||||||
</Box>
|
</Box>
|
||||||
<Box
|
<Box
|
||||||
component={Link}
|
component="span"
|
||||||
to={`/article/show/${watch("preview_article")}`}
|
|
||||||
sx={{
|
sx={{
|
||||||
color: (theme) =>
|
color: (theme) =>
|
||||||
theme.palette.mode === "dark" ? "grey.300" : "grey.800",
|
theme.palette.mode === "dark" ? "grey.300" : "grey.800",
|
||||||
textDecoration: "none",
|
|
||||||
"&:hover": {
|
|
||||||
textDecoration: "underline",
|
|
||||||
},
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{previewArticlePreview}
|
{previewArticlePreview}
|
||||||
@ -669,4 +778,4 @@ export const SightCreate = () => {
|
|||||||
</Box>
|
</Box>
|
||||||
</Create>
|
</Create>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
@ -1,4 +1,12 @@
|
|||||||
import { Autocomplete, Box, TextField, Paper, Typography } from "@mui/material";
|
import {
|
||||||
|
Autocomplete,
|
||||||
|
Box,
|
||||||
|
TextField,
|
||||||
|
Paper,
|
||||||
|
Typography,
|
||||||
|
Tab,
|
||||||
|
Tabs,
|
||||||
|
} from "@mui/material";
|
||||||
import { Edit, useAutocomplete } from "@refinedev/mui";
|
import { Edit, useAutocomplete } from "@refinedev/mui";
|
||||||
import { useForm } from "@refinedev/react-hook-form";
|
import { useForm } from "@refinedev/react-hook-form";
|
||||||
import { Controller } from "react-hook-form";
|
import { Controller } from "react-hook-form";
|
||||||
@ -9,17 +17,70 @@ import { CreateSightArticle } from "../../components/CreateSightArticle";
|
|||||||
import { ArticleItem, articleFields } from "./types";
|
import { ArticleItem, articleFields } from "./types";
|
||||||
import { TOKEN_KEY } from "../../authProvider";
|
import { TOKEN_KEY } from "../../authProvider";
|
||||||
import { Link } from "react-router";
|
import { Link } from "react-router";
|
||||||
|
import Cookies from "js-cookie";
|
||||||
|
|
||||||
|
function a11yProps(index: number) {
|
||||||
|
return {
|
||||||
|
id: `simple-tab-${index}`,
|
||||||
|
"aria-controls": `simple-tabpanel-${index}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TabPanelProps {
|
||||||
|
children?: React.ReactNode;
|
||||||
|
index: number;
|
||||||
|
value: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function CustomTabPanel(props: TabPanelProps) {
|
||||||
|
const { children, value, index, ...other } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
role="tabpanel"
|
||||||
|
hidden={value !== index}
|
||||||
|
id={`simple-tabpanel-${index}`}
|
||||||
|
aria-labelledby={`simple-tab-${index}`}
|
||||||
|
{...other}
|
||||||
|
>
|
||||||
|
{value === index && <Box sx={{ p: 3 }}>{children}</Box>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export const SightEdit = () => {
|
export const SightEdit = () => {
|
||||||
const { id: sightId } = useParams<{ id: string }>();
|
const { id: sightId } = useParams<{ id: string }>();
|
||||||
|
const [language, setLanguage] = useState(Cookies.get("lang") || "ru");
|
||||||
|
|
||||||
|
const handleLanguageChange = (lang: string) => {
|
||||||
|
setLanguage(lang);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const lang = Cookies.get("lang")!;
|
||||||
|
Cookies.set("lang", language);
|
||||||
|
return () => {
|
||||||
|
Cookies.set("lang", lang);
|
||||||
|
};
|
||||||
|
}, [language]);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
saveButtonProps,
|
saveButtonProps,
|
||||||
register,
|
register,
|
||||||
control,
|
control,
|
||||||
watch,
|
watch,
|
||||||
|
getValues,
|
||||||
|
setValue,
|
||||||
formState: { errors },
|
formState: { errors },
|
||||||
} = useForm({});
|
} = useForm({
|
||||||
|
refineCoreProps: {
|
||||||
|
meta: {
|
||||||
|
headers: {
|
||||||
|
"Accept-Language": language,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const { autocompleteProps: cityAutocompleteProps } = useAutocomplete({
|
const { autocompleteProps: cityAutocompleteProps } = useAutocomplete({
|
||||||
resource: "city",
|
resource: "city",
|
||||||
@ -30,8 +91,11 @@ export const SightEdit = () => {
|
|||||||
value,
|
value,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
queryOptions: {
|
||||||
|
queryKey: ["sight", language],
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
const [tabValue, setTabValue] = useState(0);
|
||||||
const { autocompleteProps: mediaAutocompleteProps } = useAutocomplete({
|
const { autocompleteProps: mediaAutocompleteProps } = useAutocomplete({
|
||||||
resource: "media",
|
resource: "media",
|
||||||
onSearch: (value) => [
|
onSearch: (value) => [
|
||||||
@ -45,6 +109,9 @@ export const SightEdit = () => {
|
|||||||
|
|
||||||
const { autocompleteProps: articleAutocompleteProps } = useAutocomplete({
|
const { autocompleteProps: articleAutocompleteProps } = useAutocomplete({
|
||||||
resource: "article",
|
resource: "article",
|
||||||
|
queryOptions: {
|
||||||
|
queryKey: ["article", language],
|
||||||
|
},
|
||||||
onSearch: (value) => [
|
onSearch: (value) => [
|
||||||
{
|
{
|
||||||
field: "heading",
|
field: "heading",
|
||||||
@ -54,6 +121,27 @@ export const SightEdit = () => {
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const latitude = getValues("latitude");
|
||||||
|
const longitude = getValues("longitude");
|
||||||
|
if (latitude && longitude) {
|
||||||
|
setCoordinatesPreview({
|
||||||
|
latitude: latitude,
|
||||||
|
longitude: longitude,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [getValues]);
|
||||||
|
|
||||||
|
const handleCoordinatesChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const [lat, lon] = e.target.value.split(",").map((s) => s.trim());
|
||||||
|
setCoordinatesPreview({
|
||||||
|
latitude: lat,
|
||||||
|
longitude: lon,
|
||||||
|
});
|
||||||
|
setValue("latitude", lat);
|
||||||
|
setValue("longitude", lon);
|
||||||
|
};
|
||||||
|
|
||||||
// Состояния для предпросмотра
|
// Состояния для предпросмотра
|
||||||
const [namePreview, setNamePreview] = useState("");
|
const [namePreview, setNamePreview] = useState("");
|
||||||
const [coordinatesPreview, setCoordinatesPreview] = useState({
|
const [coordinatesPreview, setCoordinatesPreview] = useState({
|
||||||
@ -72,6 +160,8 @@ export const SightEdit = () => {
|
|||||||
const [previewArticlePreview, setPreviewArticlePreview] = useState("");
|
const [previewArticlePreview, setPreviewArticlePreview] = useState("");
|
||||||
|
|
||||||
// Следим за изменениями во всех полях
|
// Следим за изменениями во всех полях
|
||||||
|
const coordinatesContent = watch("coordinates");
|
||||||
|
const addressContent = watch("address");
|
||||||
const nameContent = watch("name");
|
const nameContent = watch("name");
|
||||||
const latitudeContent = watch("latitude");
|
const latitudeContent = watch("latitude");
|
||||||
const longitudeContent = watch("longitude");
|
const longitudeContent = watch("longitude");
|
||||||
@ -155,8 +245,84 @@ export const SightEdit = () => {
|
|||||||
}, [previewArticleContent, articleAutocompleteProps.options]);
|
}, [previewArticleContent, articleAutocompleteProps.options]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<Box sx={{ display: "flex", flexDirection: "column", gap: 2 }}>
|
||||||
|
<Box sx={{ borderBottom: 1, borderColor: "divider" }}>
|
||||||
|
<Tabs
|
||||||
|
value={tabValue}
|
||||||
|
onChange={(_, newValue) => setTabValue(newValue)}
|
||||||
|
aria-label="basic tabs example"
|
||||||
|
>
|
||||||
|
<Tab label="Основная информация" {...a11yProps(0)} />
|
||||||
|
<Tab label="Статьи" {...a11yProps(1)} />
|
||||||
|
</Tabs>
|
||||||
|
</Box>
|
||||||
|
<CustomTabPanel value={tabValue} index={0}>
|
||||||
<Edit saveButtonProps={saveButtonProps}>
|
<Edit saveButtonProps={saveButtonProps}>
|
||||||
<Box sx={{ display: "flex", gap: 2 }}>
|
<Box sx={{ display: "flex", gap: 2 }}>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
flex: 1,
|
||||||
|
gap: 10,
|
||||||
|
justifyContent: "space-between",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Language Selection */}
|
||||||
|
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
gap: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
cursor: "pointer",
|
||||||
|
flex: 1,
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
bgcolor: language === "ru" ? "primary.main" : "transparent",
|
||||||
|
color: language === "ru" ? "white" : "inherit",
|
||||||
|
p: 1,
|
||||||
|
borderRadius: 1,
|
||||||
|
}}
|
||||||
|
onClick={() => handleLanguageChange("ru")}
|
||||||
|
>
|
||||||
|
RU
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
cursor: "pointer",
|
||||||
|
flex: 1,
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
bgcolor: language === "en" ? "primary.main" : "transparent",
|
||||||
|
color: language === "en" ? "white" : "inherit",
|
||||||
|
p: 1,
|
||||||
|
borderRadius: 1,
|
||||||
|
}}
|
||||||
|
onClick={() => handleLanguageChange("en")}
|
||||||
|
>
|
||||||
|
EN
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
cursor: "pointer",
|
||||||
|
flex: 1,
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
bgcolor: language === "zh" ? "primary.main" : "transparent",
|
||||||
|
color: language === "zh" ? "white" : "inherit",
|
||||||
|
p: 1,
|
||||||
|
borderRadius: 1,
|
||||||
|
}}
|
||||||
|
onClick={() => handleLanguageChange("zh")}
|
||||||
|
>
|
||||||
|
ZH
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
{/* Форма редактирования */}
|
{/* Форма редактирования */}
|
||||||
<Box
|
<Box
|
||||||
component="form"
|
component="form"
|
||||||
@ -177,19 +343,34 @@ export const SightEdit = () => {
|
|||||||
name="name"
|
name="name"
|
||||||
/>
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
{...register("latitude", {
|
value={`${coordinatesPreview.latitude}, ${coordinatesPreview.longitude}`}
|
||||||
required: "Это поле является обязательным",
|
onChange={handleCoordinatesChange}
|
||||||
valueAsNumber: true,
|
|
||||||
})}
|
|
||||||
error={!!(errors as any)?.latitude}
|
error={!!(errors as any)?.latitude}
|
||||||
helperText={(errors as any)?.latitude?.message}
|
helperText={(errors as any)?.latitude?.message}
|
||||||
margin="normal"
|
margin="normal"
|
||||||
fullWidth
|
fullWidth
|
||||||
InputLabelProps={{ shrink: true }}
|
InputLabelProps={{ shrink: true }}
|
||||||
type="number"
|
type="text"
|
||||||
label={"Широта *"}
|
label={"Координаты *"}
|
||||||
name="latitude"
|
|
||||||
/>
|
/>
|
||||||
|
<input
|
||||||
|
type="hidden"
|
||||||
|
{...register("longitude", {
|
||||||
|
value: coordinatesPreview.longitude,
|
||||||
|
required: "Это поле является обязательным",
|
||||||
|
valueAsNumber: true,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="hidden"
|
||||||
|
{...register("latitude", {
|
||||||
|
value: coordinatesPreview.latitude,
|
||||||
|
required: "Это поле является обязательным",
|
||||||
|
valueAsNumber: true,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/*
|
||||||
<TextField
|
<TextField
|
||||||
{...register("longitude", {
|
{...register("longitude", {
|
||||||
required: "Это поле является обязательным",
|
required: "Это поле является обязательным",
|
||||||
@ -203,6 +384,35 @@ export const SightEdit = () => {
|
|||||||
type="number"
|
type="number"
|
||||||
label={"Долгота *"}
|
label={"Долгота *"}
|
||||||
name="longitude"
|
name="longitude"
|
||||||
|
/> */}
|
||||||
|
|
||||||
|
{/* <TextField
|
||||||
|
{...register("coordinates", {
|
||||||
|
required: "Это поле является обязательным",
|
||||||
|
valueAsNumber: true,
|
||||||
|
})}
|
||||||
|
error={!!(errors as any)?.coordinates}
|
||||||
|
helperText={(errors as any)?.coordinates?.message}
|
||||||
|
margin="normal"
|
||||||
|
fullWidth
|
||||||
|
InputLabelProps={{ shrink: true }}
|
||||||
|
type="number"
|
||||||
|
label={"Координаты *"}
|
||||||
|
name="coordinates"
|
||||||
|
/> */}
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
{...register("address", {
|
||||||
|
required: "Это поле является обязательным",
|
||||||
|
})}
|
||||||
|
error={!!(errors as any)?.address}
|
||||||
|
helperText={(errors as any)?.address?.message}
|
||||||
|
margin="normal"
|
||||||
|
fullWidth
|
||||||
|
InputLabelProps={{ shrink: true }}
|
||||||
|
type="text"
|
||||||
|
label={"Адрес *"}
|
||||||
|
name="address"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Controller
|
<Controller
|
||||||
@ -229,7 +439,9 @@ export const SightEdit = () => {
|
|||||||
}}
|
}}
|
||||||
filterOptions={(options, { inputValue }) => {
|
filterOptions={(options, { inputValue }) => {
|
||||||
return options.filter((option) =>
|
return options.filter((option) =>
|
||||||
option.name.toLowerCase().includes(inputValue.toLowerCase())
|
option.name
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(inputValue.toLowerCase())
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
renderInput={(params) => (
|
renderInput={(params) => (
|
||||||
@ -364,7 +576,7 @@ export const SightEdit = () => {
|
|||||||
renderInput={(params) => (
|
renderInput={(params) => (
|
||||||
<TextField
|
<TextField
|
||||||
{...params}
|
{...params}
|
||||||
label="Выберите водный знак (Правый низ)"
|
label="Выберите водный знак (Правый вверх)"
|
||||||
margin="normal"
|
margin="normal"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
error={!!errors.arms}
|
error={!!errors.arms}
|
||||||
@ -462,14 +674,14 @@ export const SightEdit = () => {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
{/* Блок предпросмотра */}
|
{/* Блок предпросмотра */}
|
||||||
<Paper
|
<Paper
|
||||||
sx={{
|
sx={{
|
||||||
flex: 1,
|
flex: 1,
|
||||||
p: 2,
|
p: 2,
|
||||||
maxHeight: "calc(100vh - 200px)",
|
|
||||||
overflowY: "auto",
|
|
||||||
position: "sticky",
|
position: "sticky",
|
||||||
top: 16,
|
top: 16,
|
||||||
borderRadius: 2,
|
borderRadius: 2,
|
||||||
@ -512,6 +724,22 @@ export const SightEdit = () => {
|
|||||||
</Box>
|
</Box>
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
|
{/* Адрес */}
|
||||||
|
<Typography variant="body1" sx={{ mb: 2 }}>
|
||||||
|
<Box component="span" sx={{ color: "text.secondary" }}>
|
||||||
|
Адрес:{" "}
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
component="span"
|
||||||
|
sx={{
|
||||||
|
color: (theme) =>
|
||||||
|
theme.palette.mode === "dark" ? "grey.300" : "grey.800",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{addressContent}
|
||||||
|
</Box>
|
||||||
|
</Typography>
|
||||||
|
|
||||||
{/* Координаты */}
|
{/* Координаты */}
|
||||||
<Typography variant="body1" sx={{ mb: 2 }}>
|
<Typography variant="body1" sx={{ mb: 2 }}>
|
||||||
<Box component="span" sx={{ color: "text.secondary" }}>
|
<Box component="span" sx={{ color: "text.secondary" }}>
|
||||||
@ -524,7 +752,7 @@ export const SightEdit = () => {
|
|||||||
theme.palette.mode === "dark" ? "grey.300" : "grey.800",
|
theme.palette.mode === "dark" ? "grey.300" : "grey.800",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{coordinatesPreview.latitude}, {coordinatesPreview.longitude}
|
{`${coordinatesPreview.latitude}, ${coordinatesPreview.longitude}`}
|
||||||
</Box>
|
</Box>
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
@ -594,7 +822,7 @@ export const SightEdit = () => {
|
|||||||
gutterBottom
|
gutterBottom
|
||||||
sx={{ color: "text.secondary" }}
|
sx={{ color: "text.secondary" }}
|
||||||
>
|
>
|
||||||
Правый нижний:
|
Правый верхний:
|
||||||
</Typography>
|
</Typography>
|
||||||
<Box
|
<Box
|
||||||
component="img"
|
component="img"
|
||||||
@ -629,7 +857,9 @@ export const SightEdit = () => {
|
|||||||
to={`/article/show/${watch("left_article")}`}
|
to={`/article/show/${watch("left_article")}`}
|
||||||
sx={{
|
sx={{
|
||||||
color: (theme) =>
|
color: (theme) =>
|
||||||
theme.palette.mode === "dark" ? "grey.300" : "grey.800",
|
theme.palette.mode === "dark"
|
||||||
|
? "grey.300"
|
||||||
|
: "grey.800",
|
||||||
textDecoration: "none",
|
textDecoration: "none",
|
||||||
"&:hover": {
|
"&:hover": {
|
||||||
textDecoration: "underline",
|
textDecoration: "underline",
|
||||||
@ -650,7 +880,9 @@ export const SightEdit = () => {
|
|||||||
to={`/article/show/${watch("preview_article")}`}
|
to={`/article/show/${watch("preview_article")}`}
|
||||||
sx={{
|
sx={{
|
||||||
color: (theme) =>
|
color: (theme) =>
|
||||||
theme.palette.mode === "dark" ? "grey.300" : "grey.800",
|
theme.palette.mode === "dark"
|
||||||
|
? "grey.300"
|
||||||
|
: "grey.800",
|
||||||
textDecoration: "none",
|
textDecoration: "none",
|
||||||
"&:hover": {
|
"&:hover": {
|
||||||
textDecoration: "underline",
|
textDecoration: "underline",
|
||||||
@ -664,7 +896,9 @@ export const SightEdit = () => {
|
|||||||
</Box>
|
</Box>
|
||||||
</Paper>
|
</Paper>
|
||||||
</Box>
|
</Box>
|
||||||
|
</Edit>
|
||||||
|
</CustomTabPanel>
|
||||||
|
<CustomTabPanel value={tabValue} index={1}>
|
||||||
{sightId && (
|
{sightId && (
|
||||||
<Box sx={{ mt: 3 }}>
|
<Box sx={{ mt: 3 }}>
|
||||||
<LinkedItems<ArticleItem>
|
<LinkedItems<ArticleItem>
|
||||||
@ -684,6 +918,7 @@ export const SightEdit = () => {
|
|||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
</Edit>
|
</CustomTabPanel>
|
||||||
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,106 +1,126 @@
|
|||||||
import React from 'react'
|
import React from "react";
|
||||||
import {type GridColDef} from '@mui/x-data-grid'
|
import { type GridColDef } from "@mui/x-data-grid";
|
||||||
import {DeleteButton, EditButton, List, ShowButton, useDataGrid} from '@refinedev/mui'
|
import {
|
||||||
import {Stack} from '@mui/material'
|
DeleteButton,
|
||||||
import {CustomDataGrid} from '../../components/CustomDataGrid'
|
EditButton,
|
||||||
import {localeText} from '../../locales/ru/localeText'
|
List,
|
||||||
|
ShowButton,
|
||||||
|
useDataGrid,
|
||||||
|
} from "@refinedev/mui";
|
||||||
|
import { Stack } from "@mui/material";
|
||||||
|
import { CustomDataGrid } from "../../components/CustomDataGrid";
|
||||||
|
import { localeText } from "../../locales/ru/localeText";
|
||||||
|
import { cityStore } from "../../store/CityStore";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
|
||||||
export const SightList = () => {
|
export const SightList = observer(() => {
|
||||||
const {dataGridProps} = useDataGrid({resource: 'sight/'})
|
const { city_id } = cityStore;
|
||||||
|
const { dataGridProps } = useDataGrid({
|
||||||
|
resource: "sight/",
|
||||||
|
filters: {
|
||||||
|
permanent: [
|
||||||
|
{
|
||||||
|
field: "cityID",
|
||||||
|
operator: "eq",
|
||||||
|
value: city_id,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const columns = React.useMemo<GridColDef[]>(
|
const columns = React.useMemo<GridColDef[]>(
|
||||||
() => [
|
() => [
|
||||||
{
|
{
|
||||||
field: 'id',
|
field: "id",
|
||||||
headerName: 'ID',
|
headerName: "ID",
|
||||||
type: 'number',
|
type: "number",
|
||||||
minWidth: 70,
|
minWidth: 70,
|
||||||
display: 'flex',
|
display: "flex",
|
||||||
align: 'left',
|
align: "left",
|
||||||
headerAlign: 'left',
|
headerAlign: "left",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'name',
|
field: "name",
|
||||||
headerName: 'Название',
|
headerName: "Название",
|
||||||
type: 'string',
|
type: "string",
|
||||||
minWidth: 150,
|
minWidth: 150,
|
||||||
display: 'flex',
|
display: "flex",
|
||||||
align: 'left',
|
align: "left",
|
||||||
headerAlign: 'left',
|
headerAlign: "left",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'latitude',
|
field: "latitude",
|
||||||
headerName: 'Широта',
|
headerName: "Широта",
|
||||||
type: 'number',
|
type: "number",
|
||||||
minWidth: 150,
|
minWidth: 150,
|
||||||
display: 'flex',
|
display: "flex",
|
||||||
align: 'left',
|
align: "left",
|
||||||
headerAlign: 'left',
|
headerAlign: "left",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'longitude',
|
field: "longitude",
|
||||||
headerName: 'Долгота',
|
headerName: "Долгота",
|
||||||
type: 'number',
|
type: "number",
|
||||||
minWidth: 150,
|
minWidth: 150,
|
||||||
display: 'flex',
|
display: "flex",
|
||||||
align: 'left',
|
align: "left",
|
||||||
headerAlign: 'left',
|
headerAlign: "left",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'city_id',
|
field: "city_id",
|
||||||
headerName: 'ID города',
|
headerName: "ID города",
|
||||||
type: 'number',
|
type: "number",
|
||||||
minWidth: 70,
|
minWidth: 70,
|
||||||
display: 'flex',
|
display: "flex",
|
||||||
align: 'left',
|
align: "left",
|
||||||
headerAlign: 'left',
|
headerAlign: "left",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'city',
|
field: "city",
|
||||||
headerName: 'Город',
|
headerName: "Город",
|
||||||
type: 'string',
|
type: "string",
|
||||||
minWidth: 100,
|
minWidth: 100,
|
||||||
align: 'left',
|
align: "left",
|
||||||
headerAlign: 'left',
|
headerAlign: "left",
|
||||||
flex: 1,
|
flex: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'thumbnail',
|
field: "thumbnail",
|
||||||
headerName: 'Карточка',
|
headerName: "Карточка",
|
||||||
type: 'string',
|
type: "string",
|
||||||
minWidth: 150,
|
minWidth: 150,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'watermark_lu',
|
field: "watermark_lu",
|
||||||
headerName: 'Вод. знак (lu)',
|
headerName: "Вод. знак (lu)",
|
||||||
type: 'string',
|
type: "string",
|
||||||
minWidth: 150,
|
minWidth: 150,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'watermark_rd',
|
field: "watermark_rd",
|
||||||
headerName: 'Вод. знак (rd)',
|
headerName: "Вод. знак (rd)",
|
||||||
type: 'string',
|
type: "string",
|
||||||
minWidth: 150,
|
minWidth: 150,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'left_article',
|
field: "left_article",
|
||||||
headerName: 'Левая статья',
|
headerName: "Левая статья",
|
||||||
type: 'number',
|
type: "number",
|
||||||
minWidth: 150,
|
minWidth: 150,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'preview_article',
|
field: "preview_article",
|
||||||
headerName: 'Пред. просмотр статьи',
|
headerName: "Пред. просмотр статьи",
|
||||||
type: 'number',
|
type: "number",
|
||||||
minWidth: 150,
|
minWidth: 150,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'actions',
|
field: "actions",
|
||||||
headerName: 'Действия',
|
headerName: "Действия",
|
||||||
minWidth: 120,
|
minWidth: 120,
|
||||||
display: 'flex',
|
display: "flex",
|
||||||
align: 'right',
|
align: "right",
|
||||||
headerAlign: 'center',
|
headerAlign: "center",
|
||||||
sortable: false,
|
sortable: false,
|
||||||
filterable: false,
|
filterable: false,
|
||||||
disableColumnMenu: true,
|
disableColumnMenu: true,
|
||||||
@ -109,20 +129,30 @@ export const SightList = () => {
|
|||||||
<>
|
<>
|
||||||
<EditButton hideText recordItemId={row.id} />
|
<EditButton hideText recordItemId={row.id} />
|
||||||
<ShowButton hideText recordItemId={row.id} />
|
<ShowButton hideText recordItemId={row.id} />
|
||||||
<DeleteButton hideText confirmTitle="Вы уверены?" recordItemId={row.id} />
|
<DeleteButton
|
||||||
|
hideText
|
||||||
|
confirmTitle="Вы уверены?"
|
||||||
|
recordItemId={row.id}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[],
|
[]
|
||||||
)
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<List>
|
<List>
|
||||||
<Stack gap={2.5}>
|
<Stack gap={2.5}>
|
||||||
<CustomDataGrid {...dataGridProps} columns={columns} localeText={localeText} getRowId={(row: any) => row.id} hasCoordinates />
|
<CustomDataGrid
|
||||||
|
{...dataGridProps}
|
||||||
|
columns={columns}
|
||||||
|
localeText={localeText}
|
||||||
|
getRowId={(row: any) => row.id}
|
||||||
|
hasCoordinates
|
||||||
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
</List>
|
</List>
|
||||||
)
|
);
|
||||||
}
|
});
|
||||||
|
@ -1,22 +1,23 @@
|
|||||||
import {Stack, Typography} from '@mui/material'
|
import { Stack, Typography } from "@mui/material";
|
||||||
import {useShow} from '@refinedev/core'
|
import { useShow } from "@refinedev/core";
|
||||||
import {Show, TextFieldComponent} from '@refinedev/mui'
|
import { Show, TextFieldComponent } from "@refinedev/mui";
|
||||||
import {LinkedItems} from '../../components/LinkedItems'
|
import { LinkedItems } from "../../components/LinkedItems";
|
||||||
import {ArticleItem, articleFields} from './types'
|
import { ArticleItem, articleFields } from "./types";
|
||||||
|
|
||||||
export const SightShow = () => {
|
export const SightShow = () => {
|
||||||
const {query} = useShow({})
|
const { query } = useShow({});
|
||||||
const {data, isLoading} = query
|
const { data, isLoading } = query;
|
||||||
const record = data?.data
|
const record = data?.data;
|
||||||
|
|
||||||
const fields = [
|
const fields = [
|
||||||
// {label: 'ID', data: 'id'},
|
// {label: 'ID', data: 'id'},
|
||||||
{label: 'Название', data: 'name'},
|
{ label: "Название", data: "name" },
|
||||||
// {label: 'Широта', data: 'latitude'}, #*
|
// {label: 'Широта', data: 'latitude'}, #*
|
||||||
// {label: 'Долгота', data: 'longitude'}, #*
|
// {label: 'Долгота', data: 'longitude'}, #*
|
||||||
// {label: 'ID города', data: 'city_id'},
|
// {label: 'ID города', data: 'city_id'},
|
||||||
{label: 'Город', data: 'city'},
|
{ label: "Адрес", data: "address" },
|
||||||
]
|
{ label: "Город", data: "city" },
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Show isLoading={isLoading}>
|
<Show isLoading={isLoading}>
|
||||||
@ -30,8 +31,17 @@ export const SightShow = () => {
|
|||||||
</Stack>
|
</Stack>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{record?.id && <LinkedItems<ArticleItem> type="show" parentId={record.id} parentResource="sight" childResource="article" fields={articleFields} title="статьи" />}
|
{record?.id && (
|
||||||
|
<LinkedItems<ArticleItem>
|
||||||
|
type="show"
|
||||||
|
parentId={record.id}
|
||||||
|
parentResource="sight"
|
||||||
|
childResource="article"
|
||||||
|
fields={articleFields}
|
||||||
|
title="статьи"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
</Show>
|
</Show>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
@ -1,19 +1,28 @@
|
|||||||
import {Autocomplete, Box, TextField, Typography, Paper, Grid} from '@mui/material'
|
import {
|
||||||
import {Create, useAutocomplete} from '@refinedev/mui'
|
Autocomplete,
|
||||||
import {useForm} from '@refinedev/react-hook-form'
|
Box,
|
||||||
import {Controller} from 'react-hook-form'
|
TextField,
|
||||||
|
Typography,
|
||||||
|
FormControlLabel,
|
||||||
|
Checkbox,
|
||||||
|
Grid,
|
||||||
|
Paper,
|
||||||
|
} from "@mui/material";
|
||||||
|
import { Create, useAutocomplete } from "@refinedev/mui";
|
||||||
|
import { useForm } from "@refinedev/react-hook-form";
|
||||||
|
import { Controller } from "react-hook-form";
|
||||||
|
|
||||||
const TRANSFER_FIELDS = [
|
const TRANSFER_FIELDS = [
|
||||||
{name: 'bus', label: 'Автобус'},
|
{ name: "bus", label: "Автобус" },
|
||||||
{name: 'metro_blue', label: 'Метро (синяя)'},
|
{ name: "metro_blue", label: "Метро (синяя)" },
|
||||||
{name: 'metro_green', label: 'Метро (зеленая)'},
|
{ name: "metro_green", label: "Метро (зеленая)" },
|
||||||
{name: 'metro_orange', label: 'Метро (оранжевая)'},
|
{ name: "metro_orange", label: "Метро (оранжевая)" },
|
||||||
{name: 'metro_purple', label: 'Метро (фиолетовая)'},
|
{ name: "metro_purple", label: "Метро (фиолетовая)" },
|
||||||
{name: 'metro_red', label: 'Метро (красная)'},
|
{ name: "metro_red", label: "Метро (красная)" },
|
||||||
{name: 'train', label: 'Электричка'},
|
{ name: "train", label: "Электричка" },
|
||||||
{name: 'tram', label: 'Трамвай'},
|
{ name: "tram", label: "Трамвай" },
|
||||||
{name: 'trolleybus', label: 'Троллейбус'},
|
{ name: "trolleybus", label: "Троллейбус" },
|
||||||
]
|
];
|
||||||
|
|
||||||
export const StationCreate = () => {
|
export const StationCreate = () => {
|
||||||
const {
|
const {
|
||||||
@ -24,27 +33,31 @@ export const StationCreate = () => {
|
|||||||
formState: { errors },
|
formState: { errors },
|
||||||
} = useForm({
|
} = useForm({
|
||||||
refineCoreProps: {
|
refineCoreProps: {
|
||||||
resource: 'station/',
|
resource: "station/",
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
const { autocompleteProps: cityAutocompleteProps } = useAutocomplete({
|
const { autocompleteProps: cityAutocompleteProps } = useAutocomplete({
|
||||||
resource: 'city',
|
resource: "city",
|
||||||
onSearch: (value) => [
|
onSearch: (value) => [
|
||||||
{
|
{
|
||||||
field: 'name',
|
field: "name",
|
||||||
operator: 'contains',
|
operator: "contains",
|
||||||
value,
|
value,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
})
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Create isLoading={formLoading} saveButtonProps={saveButtonProps}>
|
<Create isLoading={formLoading} saveButtonProps={saveButtonProps}>
|
||||||
<Box component="form" sx={{display: 'flex', flexDirection: 'column'}} autoComplete="off">
|
<Box
|
||||||
|
component="form"
|
||||||
|
sx={{ display: "flex", flexDirection: "column" }}
|
||||||
|
autoComplete="off"
|
||||||
|
>
|
||||||
<TextField
|
<TextField
|
||||||
{...register('name', {
|
{...register("name", {
|
||||||
required: 'Это поле является обязательным',
|
required: "Это поле является обязательным",
|
||||||
})}
|
})}
|
||||||
error={!!(errors as any)?.name}
|
error={!!(errors as any)?.name}
|
||||||
helperText={(errors as any)?.name?.message}
|
helperText={(errors as any)?.name?.message}
|
||||||
@ -52,12 +65,12 @@ export const StationCreate = () => {
|
|||||||
fullWidth
|
fullWidth
|
||||||
InputLabelProps={{ shrink: true }}
|
InputLabelProps={{ shrink: true }}
|
||||||
type="text"
|
type="text"
|
||||||
label={'Название *'}
|
label={"Название *"}
|
||||||
name="name"
|
name="name"
|
||||||
/>
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
{...register('system_name', {
|
{...register("system_name", {
|
||||||
required: 'Это поле является обязательным',
|
required: "Это поле является обязательным",
|
||||||
})}
|
})}
|
||||||
error={!!(errors as any)?.system_name}
|
error={!!(errors as any)?.system_name}
|
||||||
helperText={(errors as any)?.system_name?.message}
|
helperText={(errors as any)?.system_name?.message}
|
||||||
@ -65,11 +78,25 @@ export const StationCreate = () => {
|
|||||||
fullWidth
|
fullWidth
|
||||||
InputLabelProps={{ shrink: true }}
|
InputLabelProps={{ shrink: true }}
|
||||||
type="text"
|
type="text"
|
||||||
label={'Системное название *'}
|
label={"Системное название *"}
|
||||||
name="system_name"
|
name="system_name"
|
||||||
/>
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
{...register('description', {
|
{...register("address", {
|
||||||
|
// required: 'Это поле является обязательным',
|
||||||
|
})}
|
||||||
|
error={!!(errors as any)?.address}
|
||||||
|
helperText={(errors as any)?.address?.message}
|
||||||
|
margin="normal"
|
||||||
|
fullWidth
|
||||||
|
InputLabelProps={{ shrink: true }}
|
||||||
|
type="text"
|
||||||
|
label={"Адрес"}
|
||||||
|
name="address"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
{...register("description", {
|
||||||
// required: 'Это поле является обязательным',
|
// required: 'Это поле является обязательным',
|
||||||
})}
|
})}
|
||||||
error={!!(errors as any)?.description}
|
error={!!(errors as any)?.description}
|
||||||
@ -78,12 +105,29 @@ export const StationCreate = () => {
|
|||||||
fullWidth
|
fullWidth
|
||||||
InputLabelProps={{ shrink: true }}
|
InputLabelProps={{ shrink: true }}
|
||||||
type="text"
|
type="text"
|
||||||
label={'Описание'}
|
label={"Описание"}
|
||||||
name="description"
|
name="description"
|
||||||
/>
|
/>
|
||||||
|
<Controller
|
||||||
|
name="direction" // boolean
|
||||||
|
control={control}
|
||||||
|
defaultValue={false}
|
||||||
|
render={({ field }: { field: any }) => (
|
||||||
|
<FormControlLabel
|
||||||
|
label="Прямой маршрут?"
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
{...field}
|
||||||
|
checked={field.value}
|
||||||
|
onChange={(e) => field.onChange(e.target.checked)}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
{...register('latitude', {
|
{...register("latitude", {
|
||||||
required: 'Это поле является обязательным',
|
required: "Это поле является обязательным",
|
||||||
valueAsNumber: true,
|
valueAsNumber: true,
|
||||||
})}
|
})}
|
||||||
error={!!(errors as any)?.latitude}
|
error={!!(errors as any)?.latitude}
|
||||||
@ -92,12 +136,12 @@ export const StationCreate = () => {
|
|||||||
fullWidth
|
fullWidth
|
||||||
InputLabelProps={{ shrink: true }}
|
InputLabelProps={{ shrink: true }}
|
||||||
type="number"
|
type="number"
|
||||||
label={'Широта *'}
|
label={"Широта *"}
|
||||||
name="latitude"
|
name="latitude"
|
||||||
/>
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
{...register('longitude', {
|
{...register("longitude", {
|
||||||
required: 'Это поле является обязательным',
|
required: "Это поле является обязательным",
|
||||||
valueAsNumber: true,
|
valueAsNumber: true,
|
||||||
})}
|
})}
|
||||||
error={!!(errors as any)?.longitude}
|
error={!!(errors as any)?.longitude}
|
||||||
@ -106,38 +150,54 @@ export const StationCreate = () => {
|
|||||||
fullWidth
|
fullWidth
|
||||||
InputLabelProps={{ shrink: true }}
|
InputLabelProps={{ shrink: true }}
|
||||||
type="number"
|
type="number"
|
||||||
label={'Долгота *'}
|
label={"Долгота *"}
|
||||||
name="longitude"
|
name="longitude"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="city_id"
|
name="city_id"
|
||||||
rules={{required: 'Это поле является обязательным'}}
|
rules={{ required: "Это поле является обязательным" }}
|
||||||
defaultValue={null}
|
defaultValue={null}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<Autocomplete
|
<Autocomplete
|
||||||
{...cityAutocompleteProps}
|
{...cityAutocompleteProps}
|
||||||
value={cityAutocompleteProps.options.find((option) => option.id === field.value) || null}
|
value={
|
||||||
|
cityAutocompleteProps.options.find(
|
||||||
|
(option) => option.id === field.value
|
||||||
|
) || null
|
||||||
|
}
|
||||||
onChange={(_, value) => {
|
onChange={(_, value) => {
|
||||||
field.onChange(value?.id || '')
|
field.onChange(value?.id || "");
|
||||||
}}
|
}}
|
||||||
getOptionLabel={(item) => {
|
getOptionLabel={(item) => {
|
||||||
return item ? item.name : ''
|
return item ? item.name : "";
|
||||||
}}
|
}}
|
||||||
isOptionEqualToValue={(option, value) => {
|
isOptionEqualToValue={(option, value) => {
|
||||||
return option.id === value?.id
|
return option.id === value?.id;
|
||||||
}}
|
}}
|
||||||
filterOptions={(options, { inputValue }) => {
|
filterOptions={(options, { inputValue }) => {
|
||||||
return options.filter((option) => option.name.toLowerCase().includes(inputValue.toLowerCase()))
|
return options.filter((option) =>
|
||||||
|
option.name.toLowerCase().includes(inputValue.toLowerCase())
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
renderInput={(params) => <TextField {...params} label="Выберите город" margin="normal" variant="outlined" error={!!errors.city_id} helperText={(errors as any)?.city_id?.message} required />}
|
renderInput={(params) => (
|
||||||
|
<TextField
|
||||||
|
{...params}
|
||||||
|
label="Выберите город"
|
||||||
|
margin="normal"
|
||||||
|
variant="outlined"
|
||||||
|
error={!!errors.city_id}
|
||||||
|
helperText={(errors as any)?.city_id?.message}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
{...register('offset_x', {
|
{...register("offset_x", {
|
||||||
// required: 'Это поле является обязательным',
|
// required: 'Это поле является обязательным',
|
||||||
})}
|
})}
|
||||||
error={!!(errors as any)?.offset_x}
|
error={!!(errors as any)?.offset_x}
|
||||||
@ -146,12 +206,12 @@ export const StationCreate = () => {
|
|||||||
fullWidth
|
fullWidth
|
||||||
InputLabelProps={{ shrink: true }}
|
InputLabelProps={{ shrink: true }}
|
||||||
type="number"
|
type="number"
|
||||||
label={'Смещение (X)'}
|
label={"Смещение (X)"}
|
||||||
name="offset_x"
|
name="offset_x"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
{...register('offset_y', {
|
{...register("offset_y", {
|
||||||
// required: 'Это поле является обязательным',
|
// required: 'Это поле является обязательным',
|
||||||
})}
|
})}
|
||||||
error={!!(errors as any)?.offset_y}
|
error={!!(errors as any)?.offset_y}
|
||||||
@ -160,7 +220,7 @@ export const StationCreate = () => {
|
|||||||
fullWidth
|
fullWidth
|
||||||
InputLabelProps={{ shrink: true }}
|
InputLabelProps={{ shrink: true }}
|
||||||
type="number"
|
type="number"
|
||||||
label={'Смещение (Y)'}
|
label={"Смещение (Y)"}
|
||||||
name="offset_y"
|
name="offset_y"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@ -172,12 +232,22 @@ export const StationCreate = () => {
|
|||||||
<Grid container spacing={2}>
|
<Grid container spacing={2}>
|
||||||
{TRANSFER_FIELDS.map((field) => (
|
{TRANSFER_FIELDS.map((field) => (
|
||||||
<Grid item xs={12} sm={6} md={4} key={field.name}>
|
<Grid item xs={12} sm={6} md={4} key={field.name}>
|
||||||
<TextField {...register(`transfers.${field.name}`)} error={!!(errors as any)?.transfers?.[field.name]} helperText={(errors as any)?.transfers?.[field.name]?.message} margin="normal" fullWidth InputLabelProps={{shrink: true}} type="text" label={field.label} name={`transfers.${field.name}`} />
|
<TextField
|
||||||
|
{...register(`transfers.${field.name}`)}
|
||||||
|
error={!!(errors as any)?.transfers?.[field.name]}
|
||||||
|
helperText={(errors as any)?.transfers?.[field.name]?.message}
|
||||||
|
margin="normal"
|
||||||
|
fullWidth
|
||||||
|
InputLabelProps={{ shrink: true }}
|
||||||
|
type="text"
|
||||||
|
label={field.label}
|
||||||
|
name={`transfers.${field.name}`}
|
||||||
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
))}
|
))}
|
||||||
</Grid>
|
</Grid>
|
||||||
</Paper>
|
</Paper>
|
||||||
</Box>
|
</Box>
|
||||||
</Create>
|
</Create>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
@ -1,23 +1,32 @@
|
|||||||
import {Autocomplete, Box, TextField, Typography, Paper, Grid} from '@mui/material'
|
import {
|
||||||
import {Edit, useAutocomplete} from '@refinedev/mui'
|
Autocomplete,
|
||||||
import {useForm} from '@refinedev/react-hook-form'
|
Box,
|
||||||
import {Controller} from 'react-hook-form'
|
TextField,
|
||||||
|
Typography,
|
||||||
|
FormControlLabel,
|
||||||
|
Paper,
|
||||||
|
Grid,
|
||||||
|
Checkbox,
|
||||||
|
} from "@mui/material";
|
||||||
|
import { Edit, useAutocomplete } from "@refinedev/mui";
|
||||||
|
import { useForm } from "@refinedev/react-hook-form";
|
||||||
|
import { Controller } from "react-hook-form";
|
||||||
|
|
||||||
import {useParams} from 'react-router'
|
import { useParams } from "react-router";
|
||||||
import {LinkedItems} from '../../components/LinkedItems'
|
import { LinkedItems } from "../../components/LinkedItems";
|
||||||
import {type SightItem, sightFields} from './types'
|
import { type SightItem, sightFields } from "./types";
|
||||||
|
|
||||||
const TRANSFER_FIELDS = [
|
const TRANSFER_FIELDS = [
|
||||||
{name: 'bus', label: 'Автобус'},
|
{ name: "bus", label: "Автобус" },
|
||||||
{name: 'metro_blue', label: 'Метро (синяя)'},
|
{ name: "metro_blue", label: "Метро (синяя)" },
|
||||||
{name: 'metro_green', label: 'Метро (зеленая)'},
|
{ name: "metro_green", label: "Метро (зеленая)" },
|
||||||
{name: 'metro_orange', label: 'Метро (оранжевая)'},
|
{ name: "metro_orange", label: "Метро (оранжевая)" },
|
||||||
{name: 'metro_purple', label: 'Метро (фиолетовая)'},
|
{ name: "metro_purple", label: "Метро (фиолетовая)" },
|
||||||
{name: 'metro_red', label: 'Метро (красная)'},
|
{ name: "metro_red", label: "Метро (красная)" },
|
||||||
{name: 'train', label: 'Электричка'},
|
{ name: "train", label: "Электричка" },
|
||||||
{name: 'tram', label: 'Трамвай'},
|
{ name: "tram", label: "Трамвай" },
|
||||||
{name: 'trolleybus', label: 'Троллейбус'},
|
{ name: "trolleybus", label: "Троллейбус" },
|
||||||
]
|
];
|
||||||
|
|
||||||
export const StationEdit = () => {
|
export const StationEdit = () => {
|
||||||
const {
|
const {
|
||||||
@ -25,27 +34,31 @@ export const StationEdit = () => {
|
|||||||
register,
|
register,
|
||||||
control,
|
control,
|
||||||
formState: { errors },
|
formState: { errors },
|
||||||
} = useForm({})
|
} = useForm({});
|
||||||
|
|
||||||
const {id: stationId} = useParams<{id: string}>()
|
const { id: stationId } = useParams<{ id: string }>();
|
||||||
|
|
||||||
const { autocompleteProps: cityAutocompleteProps } = useAutocomplete({
|
const { autocompleteProps: cityAutocompleteProps } = useAutocomplete({
|
||||||
resource: 'city',
|
resource: "city",
|
||||||
onSearch: (value) => [
|
onSearch: (value) => [
|
||||||
{
|
{
|
||||||
field: 'name',
|
field: "name",
|
||||||
operator: 'contains',
|
operator: "contains",
|
||||||
value,
|
value,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
})
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Edit saveButtonProps={saveButtonProps}>
|
<Edit saveButtonProps={saveButtonProps}>
|
||||||
<Box component="form" sx={{display: 'flex', flexDirection: 'column'}} autoComplete="off">
|
<Box
|
||||||
|
component="form"
|
||||||
|
sx={{ display: "flex", flexDirection: "column" }}
|
||||||
|
autoComplete="off"
|
||||||
|
>
|
||||||
<TextField
|
<TextField
|
||||||
{...register('name', {
|
{...register("name", {
|
||||||
required: 'Это поле является обязательным',
|
required: "Это поле является обязательным",
|
||||||
})}
|
})}
|
||||||
error={!!(errors as any)?.name}
|
error={!!(errors as any)?.name}
|
||||||
helperText={(errors as any)?.name?.message}
|
helperText={(errors as any)?.name?.message}
|
||||||
@ -53,12 +66,12 @@ export const StationEdit = () => {
|
|||||||
fullWidth
|
fullWidth
|
||||||
InputLabelProps={{ shrink: true }}
|
InputLabelProps={{ shrink: true }}
|
||||||
type="text"
|
type="text"
|
||||||
label={'Название *'}
|
label={"Название *"}
|
||||||
name="name"
|
name="name"
|
||||||
/>
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
{...register('system_name', {
|
{...register("system_name", {
|
||||||
required: 'Это поле является обязательным',
|
required: "Это поле является обязательным",
|
||||||
})}
|
})}
|
||||||
error={!!(errors as any)?.system_name}
|
error={!!(errors as any)?.system_name}
|
||||||
helperText={(errors as any)?.system_name?.message}
|
helperText={(errors as any)?.system_name?.message}
|
||||||
@ -66,11 +79,28 @@ export const StationEdit = () => {
|
|||||||
fullWidth
|
fullWidth
|
||||||
InputLabelProps={{ shrink: true }}
|
InputLabelProps={{ shrink: true }}
|
||||||
type="text"
|
type="text"
|
||||||
label={'Системное название *'}
|
label={"Системное название *"}
|
||||||
name="system_name"
|
name="system_name"
|
||||||
/>
|
/>
|
||||||
|
<Controller
|
||||||
|
name="direction" // boolean
|
||||||
|
control={control}
|
||||||
|
defaultValue={false}
|
||||||
|
render={({ field }: { field: any }) => (
|
||||||
|
<FormControlLabel
|
||||||
|
label="Прямой маршрут?"
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
{...field}
|
||||||
|
checked={field.value}
|
||||||
|
onChange={(e) => field.onChange(e.target.checked)}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
{...register('description', {
|
{...register("description", {
|
||||||
// required: 'Это поле является обязательным',
|
// required: 'Это поле является обязательным',
|
||||||
})}
|
})}
|
||||||
error={!!(errors as any)?.description}
|
error={!!(errors as any)?.description}
|
||||||
@ -79,12 +109,25 @@ export const StationEdit = () => {
|
|||||||
fullWidth
|
fullWidth
|
||||||
InputLabelProps={{ shrink: true }}
|
InputLabelProps={{ shrink: true }}
|
||||||
type="text"
|
type="text"
|
||||||
label={'Описание'}
|
label={"Описание"}
|
||||||
name="description"
|
name="description"
|
||||||
/>
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
{...register('latitude', {
|
{...register("address", {
|
||||||
required: 'Это поле является обязательным',
|
// required: 'Это поле является обязательным',
|
||||||
|
})}
|
||||||
|
error={!!(errors as any)?.address}
|
||||||
|
helperText={(errors as any)?.address?.message}
|
||||||
|
margin="normal"
|
||||||
|
fullWidth
|
||||||
|
InputLabelProps={{ shrink: true }}
|
||||||
|
type="text"
|
||||||
|
label={"Адрес"}
|
||||||
|
name="address"
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
{...register("latitude", {
|
||||||
|
required: "Это поле является обязательным",
|
||||||
valueAsNumber: true,
|
valueAsNumber: true,
|
||||||
})}
|
})}
|
||||||
error={!!(errors as any)?.latitude}
|
error={!!(errors as any)?.latitude}
|
||||||
@ -93,12 +136,12 @@ export const StationEdit = () => {
|
|||||||
fullWidth
|
fullWidth
|
||||||
InputLabelProps={{ shrink: true }}
|
InputLabelProps={{ shrink: true }}
|
||||||
type="number"
|
type="number"
|
||||||
label={'Широта *'}
|
label={"Широта *"}
|
||||||
name="latitude"
|
name="latitude"
|
||||||
/>
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
{...register('longitude', {
|
{...register("longitude", {
|
||||||
required: 'Это поле является обязательным',
|
required: "Это поле является обязательным",
|
||||||
valueAsNumber: true,
|
valueAsNumber: true,
|
||||||
})}
|
})}
|
||||||
error={!!(errors as any)?.longitude}
|
error={!!(errors as any)?.longitude}
|
||||||
@ -107,38 +150,54 @@ export const StationEdit = () => {
|
|||||||
fullWidth
|
fullWidth
|
||||||
InputLabelProps={{ shrink: true }}
|
InputLabelProps={{ shrink: true }}
|
||||||
type="number"
|
type="number"
|
||||||
label={'Долгота *'}
|
label={"Долгота *"}
|
||||||
name="longitude"
|
name="longitude"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="city_id"
|
name="city_id"
|
||||||
rules={{required: 'Это поле является обязательным'}}
|
rules={{ required: "Это поле является обязательным" }}
|
||||||
defaultValue={null}
|
defaultValue={null}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<Autocomplete
|
<Autocomplete
|
||||||
{...cityAutocompleteProps}
|
{...cityAutocompleteProps}
|
||||||
value={cityAutocompleteProps.options.find((option) => option.id === field.value) || null}
|
value={
|
||||||
|
cityAutocompleteProps.options.find(
|
||||||
|
(option) => option.id === field.value
|
||||||
|
) || null
|
||||||
|
}
|
||||||
onChange={(_, value) => {
|
onChange={(_, value) => {
|
||||||
field.onChange(value?.id || '')
|
field.onChange(value?.id || "");
|
||||||
}}
|
}}
|
||||||
getOptionLabel={(item) => {
|
getOptionLabel={(item) => {
|
||||||
return item ? item.name : ''
|
return item ? item.name : "";
|
||||||
}}
|
}}
|
||||||
isOptionEqualToValue={(option, value) => {
|
isOptionEqualToValue={(option, value) => {
|
||||||
return option.id === value?.id
|
return option.id === value?.id;
|
||||||
}}
|
}}
|
||||||
filterOptions={(options, { inputValue }) => {
|
filterOptions={(options, { inputValue }) => {
|
||||||
return options.filter((option) => option.name.toLowerCase().includes(inputValue.toLowerCase()))
|
return options.filter((option) =>
|
||||||
|
option.name.toLowerCase().includes(inputValue.toLowerCase())
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
renderInput={(params) => <TextField {...params} label="Выберите город" margin="normal" variant="outlined" error={!!errors.city_id} helperText={(errors as any)?.city_id?.message} required />}
|
renderInput={(params) => (
|
||||||
|
<TextField
|
||||||
|
{...params}
|
||||||
|
label="Выберите город"
|
||||||
|
margin="normal"
|
||||||
|
variant="outlined"
|
||||||
|
error={!!errors.city_id}
|
||||||
|
helperText={(errors as any)?.city_id?.message}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
{...register('offset_x', {
|
{...register("offset_x", {
|
||||||
// required: 'Это поле является обязательным',
|
// required: 'Это поле является обязательным',
|
||||||
})}
|
})}
|
||||||
error={!!(errors as any)?.offset_x}
|
error={!!(errors as any)?.offset_x}
|
||||||
@ -147,12 +206,12 @@ export const StationEdit = () => {
|
|||||||
fullWidth
|
fullWidth
|
||||||
InputLabelProps={{ shrink: true }}
|
InputLabelProps={{ shrink: true }}
|
||||||
type="number"
|
type="number"
|
||||||
label={'Смещение (X)'}
|
label={"Смещение (X)"}
|
||||||
name="offset_x"
|
name="offset_x"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
{...register('offset_y', {
|
{...register("offset_y", {
|
||||||
// required: 'Это поле является обязательным',
|
// required: 'Это поле является обязательным',
|
||||||
})}
|
})}
|
||||||
error={!!(errors as any)?.offset_y}
|
error={!!(errors as any)?.offset_y}
|
||||||
@ -161,7 +220,7 @@ export const StationEdit = () => {
|
|||||||
fullWidth
|
fullWidth
|
||||||
InputLabelProps={{ shrink: true }}
|
InputLabelProps={{ shrink: true }}
|
||||||
type="number"
|
type="number"
|
||||||
label={'Смещение (Y)'}
|
label={"Смещение (Y)"}
|
||||||
name="offset_y"
|
name="offset_y"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@ -173,7 +232,17 @@ export const StationEdit = () => {
|
|||||||
<Grid container spacing={2}>
|
<Grid container spacing={2}>
|
||||||
{TRANSFER_FIELDS.map((field) => (
|
{TRANSFER_FIELDS.map((field) => (
|
||||||
<Grid item xs={12} sm={6} md={4} key={field.name}>
|
<Grid item xs={12} sm={6} md={4} key={field.name}>
|
||||||
<TextField {...register(`transfers.${field.name}`)} error={!!(errors as any)?.transfers?.[field.name]} helperText={(errors as any)?.transfers?.[field.name]?.message} margin="normal" fullWidth InputLabelProps={{shrink: true}} type="text" label={field.label} name={`transfers.${field.name}`} />
|
<TextField
|
||||||
|
{...register(`transfers.${field.name}`)}
|
||||||
|
error={!!(errors as any)?.transfers?.[field.name]}
|
||||||
|
helperText={(errors as any)?.transfers?.[field.name]?.message}
|
||||||
|
margin="normal"
|
||||||
|
fullWidth
|
||||||
|
InputLabelProps={{ shrink: true }}
|
||||||
|
type="text"
|
||||||
|
label={field.label}
|
||||||
|
name={`transfers.${field.name}`}
|
||||||
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
))}
|
))}
|
||||||
</Grid>
|
</Grid>
|
||||||
@ -188,8 +257,9 @@ export const StationEdit = () => {
|
|||||||
childResource="sight"
|
childResource="sight"
|
||||||
fields={sightFields}
|
fields={sightFields}
|
||||||
title="достопримечательности"
|
title="достопримечательности"
|
||||||
|
dragAllowed={false}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Edit>
|
</Edit>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
@ -1,104 +1,125 @@
|
|||||||
import React from 'react'
|
import React, { useMemo } from "react";
|
||||||
import {type GridColDef} from '@mui/x-data-grid'
|
import { type GridColDef } from "@mui/x-data-grid";
|
||||||
import {DeleteButton, EditButton, List, ShowButton, useDataGrid} from '@refinedev/mui'
|
import {
|
||||||
import {Stack} from '@mui/material'
|
DeleteButton,
|
||||||
import {CustomDataGrid} from '../../components/CustomDataGrid'
|
EditButton,
|
||||||
import {localeText} from '../../locales/ru/localeText'
|
List,
|
||||||
|
ShowButton,
|
||||||
|
useDataGrid,
|
||||||
|
} from "@refinedev/mui";
|
||||||
|
import { Stack } from "@mui/material";
|
||||||
|
import { CustomDataGrid } from "../../components/CustomDataGrid";
|
||||||
|
import { localeText } from "../../locales/ru/localeText";
|
||||||
|
import { cityStore } from "../../store/CityStore";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
|
||||||
export const StationList = () => {
|
export const StationList = observer(() => {
|
||||||
const {dataGridProps} = useDataGrid({resource: 'station/'})
|
const { city_id } = cityStore;
|
||||||
|
|
||||||
|
const { dataGridProps } = useDataGrid({
|
||||||
|
resource: "station",
|
||||||
|
filters: {
|
||||||
|
permanent: [
|
||||||
|
{
|
||||||
|
field: "cityID",
|
||||||
|
operator: "eq",
|
||||||
|
value: city_id,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const columns = React.useMemo<GridColDef[]>(
|
const columns = React.useMemo<GridColDef[]>(
|
||||||
() => [
|
() => [
|
||||||
{
|
{
|
||||||
field: 'id',
|
field: "id",
|
||||||
headerName: 'ID',
|
headerName: "ID",
|
||||||
type: 'number',
|
type: "number",
|
||||||
minWidth: 70,
|
minWidth: 70,
|
||||||
display: 'flex',
|
display: "flex",
|
||||||
align: 'left',
|
align: "left",
|
||||||
headerAlign: 'left',
|
headerAlign: "left",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'name',
|
field: "name",
|
||||||
headerName: 'Название',
|
headerName: "Название",
|
||||||
type: 'string',
|
type: "string",
|
||||||
minWidth: 300,
|
minWidth: 300,
|
||||||
display: 'flex',
|
display: "flex",
|
||||||
align: 'left',
|
align: "left",
|
||||||
headerAlign: 'left',
|
headerAlign: "left",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'system_name',
|
field: "system_name",
|
||||||
headerName: 'Системное название',
|
headerName: "Системное название",
|
||||||
type: 'string',
|
type: "string",
|
||||||
minWidth: 200,
|
minWidth: 200,
|
||||||
display: 'flex',
|
display: "flex",
|
||||||
align: 'left',
|
align: "left",
|
||||||
headerAlign: 'left',
|
headerAlign: "left",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'latitude',
|
field: "latitude",
|
||||||
headerName: 'Широта',
|
headerName: "Широта",
|
||||||
type: 'number',
|
type: "number",
|
||||||
minWidth: 150,
|
minWidth: 150,
|
||||||
display: 'flex',
|
display: "flex",
|
||||||
align: 'left',
|
align: "left",
|
||||||
headerAlign: 'left',
|
headerAlign: "left",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'longitude',
|
field: "longitude",
|
||||||
headerName: 'Долгота',
|
headerName: "Долгота",
|
||||||
type: 'number',
|
type: "number",
|
||||||
minWidth: 150,
|
minWidth: 150,
|
||||||
display: 'flex',
|
display: "flex",
|
||||||
align: 'left',
|
align: "left",
|
||||||
headerAlign: 'left',
|
headerAlign: "left",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'city_id',
|
field: "city_id",
|
||||||
headerName: 'ID города',
|
headerName: "ID города",
|
||||||
type: 'number',
|
type: "number",
|
||||||
minWidth: 120,
|
minWidth: 120,
|
||||||
display: 'flex',
|
display: "flex",
|
||||||
align: 'left',
|
align: "left",
|
||||||
headerAlign: 'left',
|
headerAlign: "left",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'offset_x',
|
field: "offset_x",
|
||||||
headerName: 'Смещение (X)',
|
headerName: "Смещение (X)",
|
||||||
type: 'number',
|
type: "number",
|
||||||
minWidth: 120,
|
minWidth: 120,
|
||||||
display: 'flex',
|
display: "flex",
|
||||||
align: 'left',
|
align: "left",
|
||||||
headerAlign: 'left',
|
headerAlign: "left",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'offset_y',
|
field: "offset_y",
|
||||||
headerName: 'Смещение (Y)',
|
headerName: "Смещение (Y)",
|
||||||
type: 'number',
|
type: "number",
|
||||||
minWidth: 120,
|
minWidth: 120,
|
||||||
display: 'flex',
|
display: "flex",
|
||||||
align: 'left',
|
align: "left",
|
||||||
headerAlign: 'left',
|
headerAlign: "left",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'description',
|
field: "description",
|
||||||
headerName: 'Описание',
|
headerName: "Описание",
|
||||||
type: 'string',
|
type: "string",
|
||||||
display: 'flex',
|
display: "flex",
|
||||||
align: 'left',
|
align: "left",
|
||||||
headerAlign: 'left',
|
headerAlign: "left",
|
||||||
flex: 1,
|
flex: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'actions',
|
field: "actions",
|
||||||
headerName: 'Действия',
|
headerName: "Действия",
|
||||||
cellClassName: 'station-actions',
|
cellClassName: "station-actions",
|
||||||
align: 'right',
|
align: "right",
|
||||||
headerAlign: 'center',
|
headerAlign: "center",
|
||||||
minWidth: 120,
|
minWidth: 120,
|
||||||
display: 'flex',
|
display: "flex",
|
||||||
sortable: false,
|
sortable: false,
|
||||||
filterable: false,
|
filterable: false,
|
||||||
disableColumnMenu: true,
|
disableColumnMenu: true,
|
||||||
@ -107,20 +128,30 @@ export const StationList = () => {
|
|||||||
<>
|
<>
|
||||||
<EditButton hideText recordItemId={row.id} />
|
<EditButton hideText recordItemId={row.id} />
|
||||||
<ShowButton hideText recordItemId={row.id} />
|
<ShowButton hideText recordItemId={row.id} />
|
||||||
<DeleteButton hideText confirmTitle="Вы уверены?" recordItemId={row.id} />
|
<DeleteButton
|
||||||
|
hideText
|
||||||
|
confirmTitle="Вы уверены?"
|
||||||
|
recordItemId={row.id}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[],
|
[]
|
||||||
)
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<List>
|
<List key={city_id}>
|
||||||
<Stack gap={2.5}>
|
<Stack gap={2.5}>
|
||||||
<CustomDataGrid {...dataGridProps} columns={columns} localeText={localeText} getRowId={(row: any) => row.id} hasCoordinates />
|
<CustomDataGrid
|
||||||
|
{...dataGridProps}
|
||||||
|
columns={columns}
|
||||||
|
localeText={localeText}
|
||||||
|
getRowId={(row: any) => row.id}
|
||||||
|
hasCoordinates
|
||||||
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
</List>
|
</List>
|
||||||
)
|
);
|
||||||
}
|
});
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
import {useShow} from '@refinedev/core'
|
import { useShow } from "@refinedev/core";
|
||||||
import {Show, TextFieldComponent as TextField} from '@refinedev/mui'
|
import { Show, TextFieldComponent as TextField } from "@refinedev/mui";
|
||||||
import {Stack, Typography} from '@mui/material'
|
import { Box, Stack, Typography } from "@mui/material";
|
||||||
import {LinkedItems} from '../../components/LinkedItems'
|
import { LinkedItems } from "../../components/LinkedItems";
|
||||||
import {type SightItem, sightFields, stationFields} from './types'
|
import { type SightItem, sightFields, stationFields } from "./types";
|
||||||
|
|
||||||
export const StationShow = () => {
|
export const StationShow = () => {
|
||||||
const {query} = useShow({})
|
const { query } = useShow({});
|
||||||
const {data, isLoading} = query
|
const { data, isLoading } = query;
|
||||||
const record = data?.data
|
const record = data?.data;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Show isLoading={isLoading}>
|
<Show isLoading={isLoading}>
|
||||||
@ -16,8 +16,16 @@ export const StationShow = () => {
|
|||||||
<Stack key={data} gap={1}>
|
<Stack key={data} gap={1}>
|
||||||
<Typography variant="body1" fontWeight="bold">
|
<Typography variant="body1" fontWeight="bold">
|
||||||
{label}
|
{label}
|
||||||
|
{label === "Системное название" && (
|
||||||
|
<Box>
|
||||||
|
<TextField
|
||||||
|
value={record?.direction ? "(Прямой)" : "(Обратный)"}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
</Typography>
|
</Typography>
|
||||||
<TextField value={record?.[data] || ''} />
|
|
||||||
|
<TextField value={record?.[data] || ""} />
|
||||||
</Stack>
|
</Stack>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
@ -33,5 +41,5 @@ export const StationShow = () => {
|
|||||||
)}
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
</Show>
|
</Show>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
@ -1,44 +1,45 @@
|
|||||||
import React from 'react'
|
import React from "react";
|
||||||
|
|
||||||
export type StationItem = {
|
export type StationItem = {
|
||||||
id: number
|
id: number;
|
||||||
name: string
|
name: string;
|
||||||
description: string
|
description: string;
|
||||||
latitude: number
|
latitude: number;
|
||||||
longitude: number
|
longitude: number;
|
||||||
[key: string]: string | number
|
[key: string]: string | number;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type SightItem = {
|
export type SightItem = {
|
||||||
id: number
|
id: number;
|
||||||
name: string
|
name: string;
|
||||||
latitude: number
|
latitude: number;
|
||||||
longitude: number
|
longitude: number;
|
||||||
city_id: number
|
city_id: number;
|
||||||
city: string
|
city: string;
|
||||||
[key: string]: string | number
|
[key: string]: string | number;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type FieldType<T> = {
|
export type FieldType<T> = {
|
||||||
label: string
|
label: string;
|
||||||
data: keyof T
|
data: keyof T;
|
||||||
render?: (value: any) => React.ReactNode
|
render?: (value: any) => React.ReactNode;
|
||||||
}
|
};
|
||||||
|
|
||||||
export const stationFields: Array<FieldType<StationItem>> = [
|
export const stationFields: Array<FieldType<StationItem>> = [
|
||||||
// {label: 'ID', data: 'id'},
|
// {label: 'ID', data: 'id'},
|
||||||
{label: 'Название', data: 'name'},
|
{ label: "Название", data: "name" },
|
||||||
{label: 'Системное название', data: 'system_name'},
|
{ label: "Системное название", data: "system_name" },
|
||||||
|
{ label: "Адрес", data: "address" },
|
||||||
// {label: 'Широта', data: 'latitude'},
|
// {label: 'Широта', data: 'latitude'},
|
||||||
// {label: 'Долгота', data: 'longitude'},
|
// {label: 'Долгота', data: 'longitude'},
|
||||||
{label: 'Описание', data: 'description'},
|
{ label: "Описание", data: "description" },
|
||||||
]
|
];
|
||||||
|
|
||||||
export const sightFields: Array<FieldType<SightItem>> = [
|
export const sightFields: Array<FieldType<SightItem>> = [
|
||||||
// {label: 'ID', data: 'id'},
|
// {label: 'ID', data: 'id'},
|
||||||
{label: 'Название', data: 'name'},
|
{ label: "Название", data: "name" },
|
||||||
// {label: 'Широта', data: 'latitude'},
|
// {label: 'Широта', data: 'latitude'},
|
||||||
// {label: 'Долгота', data: 'longitude'},
|
// {label: 'Долгота', data: 'longitude'},
|
||||||
// {label: 'ID города', data: 'city_id'},
|
// {label: 'ID города', data: 'city_id'},
|
||||||
{label: 'Город', data: 'city'},
|
{ label: "Город", data: "city" },
|
||||||
]
|
];
|
||||||
|
@ -1,14 +1,22 @@
|
|||||||
import dataProvider from "@refinedev/simple-rest";
|
import dataProvider from "@refinedev/simple-rest";
|
||||||
import axios from "axios";
|
import axios, { InternalAxiosRequestConfig } from "axios";
|
||||||
|
|
||||||
import { TOKEN_KEY } from "../authProvider";
|
import { TOKEN_KEY } from "../authProvider";
|
||||||
import Cookies from "js-cookie";
|
import Cookies from "js-cookie";
|
||||||
|
|
||||||
|
interface CustomAxiosRequestConfig extends InternalAxiosRequestConfig {
|
||||||
|
meta?: {
|
||||||
|
headers?: {
|
||||||
|
"X-Language"?: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export const axiosInstance = axios.create({
|
export const axiosInstance = axios.create({
|
||||||
baseURL: import.meta.env.VITE_KRBL_API,
|
baseURL: import.meta.env.VITE_KRBL_API,
|
||||||
});
|
});
|
||||||
|
|
||||||
axiosInstance.interceptors.request.use((config) => {
|
axiosInstance.interceptors.request.use((config: CustomAxiosRequestConfig) => {
|
||||||
// Добавляем токен авторизации
|
// Добавляем токен авторизации
|
||||||
const token = localStorage.getItem(TOKEN_KEY);
|
const token = localStorage.getItem(TOKEN_KEY);
|
||||||
if (token) {
|
if (token) {
|
||||||
@ -16,9 +24,15 @@ axiosInstance.interceptors.request.use((config) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Добавляем язык в кастомный заголовок
|
// Добавляем язык в кастомный заголовок
|
||||||
|
const metaLang = config.meta?.headers?.["X-Language"];
|
||||||
|
if (metaLang) {
|
||||||
|
console.log("metaLang", metaLang);
|
||||||
|
config.headers["X-Language"] = metaLang;
|
||||||
|
} else {
|
||||||
const lang = Cookies.get("lang") || "ru";
|
const lang = Cookies.get("lang") || "ru";
|
||||||
config.headers["X-Language"] = lang; // или 'Accept-Language'
|
console.log("lang", lang);
|
||||||
|
config.headers["X-Language"] = lang;
|
||||||
|
}
|
||||||
// console.log('Request headers:', config.headers)
|
// console.log('Request headers:', config.headers)
|
||||||
|
|
||||||
return config;
|
return config;
|
||||||
|
21
src/store/CityStore.ts
Normal file
21
src/store/CityStore.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { makeAutoObservable } from "mobx";
|
||||||
|
|
||||||
|
class CityStore {
|
||||||
|
city_id: string = "";
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
makeAutoObservable(this);
|
||||||
|
this.initialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
initialize() {
|
||||||
|
this.city_id = localStorage.getItem("city_id") || "1";
|
||||||
|
}
|
||||||
|
|
||||||
|
setCityIdAction = (city_id: string) => {
|
||||||
|
this.city_id = city_id;
|
||||||
|
localStorage.setItem("city_id", city_id);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const cityStore = new CityStore();
|
12
yarn.lock
12
yarn.lock
@ -4789,6 +4789,18 @@ mkdirp@^0.5.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
minimist "^1.2.6"
|
minimist "^1.2.6"
|
||||||
|
|
||||||
|
mobx-react-lite@^4.1.0:
|
||||||
|
version "4.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/mobx-react-lite/-/mobx-react-lite-4.1.0.tgz#6a03ed2d94150848213cfebd7d172e123528a972"
|
||||||
|
integrity sha512-QEP10dpHHBeQNv1pks3WnHRCem2Zp636lq54M2nKO2Sarr13pL4u6diQXf65yzXUn0mkk18SyIDCm9UOJYTi1w==
|
||||||
|
dependencies:
|
||||||
|
use-sync-external-store "^1.4.0"
|
||||||
|
|
||||||
|
mobx@^6.13.7:
|
||||||
|
version "6.13.7"
|
||||||
|
resolved "https://registry.yarnpkg.com/mobx/-/mobx-6.13.7.tgz#70e5dda7a45da947f773b3cd3b065dfe7c8a75de"
|
||||||
|
integrity sha512-aChaVU/DO5aRPmk1GX8L+whocagUUpBQqoPtJk+cm7UOXUk87J4PeWCh6nNmTTIfEhiR9DI/+FnA8dln/hTK7g==
|
||||||
|
|
||||||
ms@2.0.0:
|
ms@2.0.0:
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
|
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
|
||||||
|
Loading…
Reference in New Issue
Block a user