sight edit update

This commit is contained in:
Илья Куприец 2025-04-25 05:23:07 +03:00
parent 463c593a0e
commit 9927c0afd6
22 changed files with 3011 additions and 1742 deletions

View File

@ -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",

View File

@ -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>
) );
} };

View File

@ -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" && (

View File

@ -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>
) );
} }
);

View File

@ -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>
) );
} };

View File

@ -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

View File

@ -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>
) );
} };

View File

@ -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>
) );
} });

View File

@ -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>
) );
} };

View File

@ -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,
}, },
] ];

View File

@ -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>
); );
}; });

View File

@ -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>
); );
}; };

View File

@ -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>
) );
} });

View File

@ -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>
) );
} };

View File

@ -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>
) );
} };

View File

@ -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>
) );
} };

View File

@ -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>
) );
} });

View File

@ -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>
) );
} };

View File

@ -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" },
] ];

View File

@ -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
View 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();

View File

@ -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"