Compare commits
4 Commits
5bf3124fd4
...
5e9e40aad2
| Author | SHA1 | Date |
|---|---|---|
|
|
5e9e40aad2 | |
|
|
405bda3df9 | |
|
|
328018edfa | |
|
|
69a5e4ade1 |
|
|
@ -0,0 +1,258 @@
|
|||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import {
|
||||
TextField, Box, Typography, IconButton, Divider,
|
||||
CircularProgress, Alert, Collapse, Tooltip, Button
|
||||
} from '@mui/material';
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import SearchIcon from '@mui/icons-material/Search';
|
||||
import axios from 'axios';
|
||||
|
||||
const MetricRangeEditor = ({ onSave }) => {
|
||||
const [ranges, setRanges] = useState([]);
|
||||
const [filter, setFilter] = useState('');
|
||||
const [newMetricName, setNewMetricName] = useState('');
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState(null);
|
||||
const [hasChanges, setHasChanges] = useState(false);
|
||||
const [success, setSuccess] = useState(false);
|
||||
|
||||
// Загрузка данных
|
||||
const loadRanges = useCallback(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const res = await axios.get(`${import.meta.env.VITE_BACK_URL}/api/ranges/list`);
|
||||
setRanges(
|
||||
Object.entries(res.data).map(([name, r]) => ({
|
||||
name,
|
||||
ranges: Array.isArray(r) ? r : []
|
||||
}))
|
||||
);
|
||||
setError(null);
|
||||
} catch (err) {
|
||||
console.error('Ошибка при получении ranges:', err);
|
||||
setError('Не удалось загрузить данные');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
loadRanges();
|
||||
}, [loadRanges]);
|
||||
|
||||
// Обновление диапазона
|
||||
const updateRange = (metricIndex, rangeIndex, field, value) => {
|
||||
const newRanges = [...ranges];
|
||||
newRanges[metricIndex].ranges[rangeIndex][field] = Number(value);
|
||||
setRanges(newRanges);
|
||||
setHasChanges(true);
|
||||
};
|
||||
|
||||
// Добавление диапазона
|
||||
const addRange = (metricIndex) => {
|
||||
const newRanges = [...ranges];
|
||||
newRanges[metricIndex].ranges.push({ min: 0, max: 100, status: 1 });
|
||||
setRanges(newRanges);
|
||||
setHasChanges(true);
|
||||
};
|
||||
|
||||
// Удаление диапазона
|
||||
const deleteRange = (metricIndex, rangeIndex) => {
|
||||
const newRanges = [...ranges];
|
||||
newRanges[metricIndex].ranges.splice(rangeIndex, 1);
|
||||
setRanges(newRanges);
|
||||
setHasChanges(true);
|
||||
};
|
||||
|
||||
const saveChanges = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
await axios.post(`${import.meta.env.VITE_BACK_URL}/api/ranges/update`, ranges);
|
||||
setHasChanges(false);
|
||||
setSuccess(true);
|
||||
setTimeout(() => setSuccess(false), 3000);
|
||||
|
||||
if (onSave) {
|
||||
onSave({
|
||||
hasChanges: false,
|
||||
saveChanges: saveChanges
|
||||
});
|
||||
}
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.error('Ошибка при сохранении:', err);
|
||||
setError('Ошибка при сохранении');
|
||||
return false;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Добавление новой метрики
|
||||
const addNewMetric = () => {
|
||||
if (!newMetricName.trim()) {
|
||||
setError('Введите название метрики');
|
||||
return;
|
||||
}
|
||||
if (ranges.some(r => r.name === newMetricName)) {
|
||||
setError('Метрика с таким именем уже существует');
|
||||
return;
|
||||
}
|
||||
setRanges([...ranges, {
|
||||
name: newMetricName,
|
||||
ranges: [{ min: 0, max: 100, status: 1 }]
|
||||
}]);
|
||||
setNewMetricName('');
|
||||
setHasChanges(true);
|
||||
setError(null);
|
||||
};
|
||||
|
||||
const filtered = filter
|
||||
? ranges.filter(r => r.name.toLowerCase().includes(filter.toLowerCase()))
|
||||
: ranges;
|
||||
|
||||
return (
|
||||
<Box sx={{ position: 'relative' }}>
|
||||
{loading && (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', p: 3 }}>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Collapse in={!!error}>
|
||||
<Alert severity="error" sx={{ mb: 2 }} onClose={() => setError(null)}>
|
||||
{error}
|
||||
</Alert>
|
||||
</Collapse>
|
||||
|
||||
<Collapse in={success}>
|
||||
<Alert severity="success" sx={{ mb: 2 }} onClose={() => setSuccess(false)}>
|
||||
Изменения успешно сохранены!
|
||||
</Alert>
|
||||
</Collapse>
|
||||
|
||||
{!loading && (
|
||||
<>
|
||||
<Box sx={{ display: 'flex', alignItems: 'flex-end', gap: 1, mb: 2 }}>
|
||||
<SearchIcon sx={{ color: 'action.active', mr: 1 }} />
|
||||
<TextField
|
||||
label="Поиск по метрике"
|
||||
fullWidth
|
||||
value={filter}
|
||||
onChange={(e) => setFilter(e.target.value)}
|
||||
variant="standard"
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: 'flex', gap: 2, alignItems: 'center', mb: 3 }}>
|
||||
<TextField
|
||||
label="Новая метрика"
|
||||
value={newMetricName}
|
||||
onChange={(e) => setNewMetricName(e.target.value)}
|
||||
fullWidth
|
||||
variant="standard"
|
||||
/>
|
||||
<Tooltip title="Добавить метрику">
|
||||
<IconButton
|
||||
onClick={addNewMetric}
|
||||
color="primary"
|
||||
disabled={!newMetricName.trim()}
|
||||
>
|
||||
<AddIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
|
||||
<Divider sx={{ mb: 3 }} />
|
||||
|
||||
{filtered.map((metric, i) => (
|
||||
<Box
|
||||
key={metric.name}
|
||||
sx={{
|
||||
mb: 3,
|
||||
p: 2,
|
||||
border: '1px solid',
|
||||
borderColor: 'divider',
|
||||
borderRadius: 2,
|
||||
backgroundColor: 'background.paper'
|
||||
}}
|
||||
>
|
||||
<Typography variant="subtitle1" fontWeight="bold" gutterBottom>
|
||||
{metric.name}
|
||||
</Typography>
|
||||
|
||||
{metric.ranges.map((r, j) => (
|
||||
<Box
|
||||
key={j}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
gap: 2,
|
||||
alignItems: 'center',
|
||||
mt: 1,
|
||||
'& > *': { flex: 1 }
|
||||
}}
|
||||
>
|
||||
<TextField
|
||||
label="Минимум"
|
||||
type="number"
|
||||
value={r.min}
|
||||
onChange={(e) => updateRange(i, j, 'min', e.target.value)}
|
||||
size="small"
|
||||
/>
|
||||
<TextField
|
||||
label="Максимум"
|
||||
type="number"
|
||||
value={r.max}
|
||||
onChange={(e) => updateRange(i, j, 'max', e.target.value)}
|
||||
size="small"
|
||||
/>
|
||||
<TextField
|
||||
label="Статус"
|
||||
type="number"
|
||||
value={r.status}
|
||||
onChange={(e) => updateRange(i, j, 'status', e.target.value)}
|
||||
size="small"
|
||||
/>
|
||||
<Tooltip title="Удалить диапазон">
|
||||
<IconButton
|
||||
onClick={() => deleteRange(i, j)}
|
||||
size="small"
|
||||
sx={{ flex: 'none' }}
|
||||
>
|
||||
<DeleteIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
))}
|
||||
|
||||
<Button
|
||||
onClick={() => addRange(i)}
|
||||
startIcon={<AddIcon />}
|
||||
size="small"
|
||||
sx={{ mt: 1 }}
|
||||
>
|
||||
Добавить диапазон
|
||||
</Button>
|
||||
</Box>
|
||||
))}
|
||||
|
||||
{filtered.length === 0 && (
|
||||
<Typography color="text.secondary" textAlign="center" py={3}>
|
||||
{filter ? 'Ничего не найдено' : 'Нет метрик для отображения'}
|
||||
</Typography>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Передаем состояние изменений в родительский компонент */}
|
||||
{useEffect(() => {
|
||||
if (onSave) {
|
||||
onSave({ hasChanges, saveChanges });
|
||||
}
|
||||
}, [hasChanges, onSave])}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default MetricRangeEditor;
|
||||
|
|
@ -0,0 +1,215 @@
|
|||
// components/SettingsModal.jsx
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
Button,
|
||||
Tabs,
|
||||
Tab,
|
||||
Box,
|
||||
Typography,
|
||||
IconButton,
|
||||
styled,
|
||||
CircularProgress,
|
||||
Slide,
|
||||
Snackbar,
|
||||
Alert
|
||||
} from '@mui/material';
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
import SaveIcon from '@mui/icons-material/Save';
|
||||
import MetricRangeEditor from './SettingsComponents/MetricRangeEditor';
|
||||
|
||||
const Transition = React.forwardRef(function Transition(props, ref) {
|
||||
return <Slide direction="up" ref={ref} {...props} />;
|
||||
});
|
||||
|
||||
const StyledDialog = styled(Dialog)(({ theme }) => ({
|
||||
'& .MuiDialog-paper': {
|
||||
minWidth: 600,
|
||||
maxHeight: '80vh',
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
},
|
||||
}));
|
||||
|
||||
const TabPanel = (props) => {
|
||||
const { children, value, index, ...other } = props;
|
||||
|
||||
return (
|
||||
<div
|
||||
role="tabpanel"
|
||||
hidden={value !== index}
|
||||
id={`settings-tabpanel-${index}`}
|
||||
aria-labelledby={`settings-tab-${index}`}
|
||||
{...other}
|
||||
>
|
||||
{value === index && (
|
||||
<Box sx={{ p: 3 }}>
|
||||
<Typography component="div">{children}</Typography>
|
||||
</Box>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const SettingsModal = ({ open, onClose, onMenuUpdate }) => {
|
||||
const [tabValue, setTabValue] = useState(0);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [showSuccess, setShowSuccess] = useState(false);
|
||||
const [hasChanges, setHasChanges] = useState(false);
|
||||
const [showConfirmClose, setShowConfirmClose] = useState(false);
|
||||
const [metricEditorState, setMetricEditorState] = useState({
|
||||
hasChanges: false,
|
||||
save: () => { }
|
||||
});
|
||||
|
||||
const handleTabChange = (event, newValue) => {
|
||||
if (hasChanges) {
|
||||
setShowConfirmClose(true);
|
||||
} else {
|
||||
setTabValue(newValue);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
setIsSaving(true);
|
||||
try {
|
||||
let success = true;
|
||||
if (tabValue === 1 && metricEditorState.hasChanges) {
|
||||
success = await metricEditorState.save();
|
||||
}
|
||||
|
||||
if (success) {
|
||||
setShowSuccess(true);
|
||||
setHasChanges(false);
|
||||
if (onMenuUpdate) {
|
||||
onMenuUpdate();
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleMetricEditorChange = ({ hasChanges, saveChanges }) => {
|
||||
setMetricEditorState({ hasChanges, save: saveChanges });
|
||||
setHasChanges(hasChanges);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
if (hasChanges) {
|
||||
setShowConfirmClose(true);
|
||||
} else {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
const handleConfirmClose = (shouldClose) => {
|
||||
setShowConfirmClose(false);
|
||||
if (shouldClose) {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
// Пример обработчика изменений
|
||||
const handleSettingChange = () => {
|
||||
setHasChanges(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<StyledDialog
|
||||
open={open}
|
||||
onClose={handleClose}
|
||||
aria-labelledby="settings-dialog-title"
|
||||
maxWidth="md"
|
||||
fullWidth
|
||||
TransitionComponent={Transition}
|
||||
>
|
||||
<DialogTitle id="settings-dialog-title">
|
||||
Настройки
|
||||
<IconButton
|
||||
aria-label="close"
|
||||
onClick={handleClose}
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
right: 8,
|
||||
top: 8,
|
||||
color: (theme) => theme.palette.grey[500],
|
||||
}}
|
||||
>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
</DialogTitle>
|
||||
|
||||
<Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
|
||||
<Tabs value={tabValue} onChange={handleTabChange} aria-label="settings tabs">
|
||||
<Tab label="Меню" id="settings-tab-0" aria-controls="settings-tabpanel-0" />
|
||||
<Tab label="Границы метрик" id="settings-tab-1" aria-controls="settings-tabpanel-1" />
|
||||
{/* Добавляйте новые вкладки здесь */}
|
||||
</Tabs>
|
||||
</Box>
|
||||
|
||||
<DialogContent dividers>
|
||||
<TabPanel value={tabValue} index={0}>
|
||||
<Typography variant="h6">Настройки меню</Typography>
|
||||
{/* Добавьте содержимое для вкладки меню */}
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel value={tabValue} index={1}>
|
||||
<MetricRangeEditor onSave={handleMetricEditorChange} />
|
||||
</TabPanel>
|
||||
|
||||
{/* Добавляйте новые TabPanel для новых вкладок */}
|
||||
</DialogContent>
|
||||
|
||||
<DialogActions>
|
||||
<Button onClick={handleClose}>Закрыть</Button>
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
variant="contained"
|
||||
color="primary"
|
||||
disabled={isSaving || !hasChanges}
|
||||
startIcon={isSaving ? <CircularProgress size={20} /> : <SaveIcon />}
|
||||
>
|
||||
{isSaving ? 'Сохранение...' : 'Сохранить'}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</StyledDialog>
|
||||
|
||||
{/* Уведомление об успешном сохранении */}
|
||||
<Snackbar
|
||||
open={showSuccess}
|
||||
autoHideDuration={3000}
|
||||
onClose={() => setShowSuccess(false)}
|
||||
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
|
||||
>
|
||||
<Alert onClose={() => setShowSuccess(false)} severity="success" sx={{ width: '100%' }}>
|
||||
Настройки успешно сохранены!
|
||||
</Alert>
|
||||
</Snackbar>
|
||||
|
||||
{/* Диалог подтверждения закрытия */}
|
||||
<Dialog
|
||||
open={showConfirmClose}
|
||||
onClose={() => handleConfirmClose(false)}
|
||||
aria-labelledby="alert-dialog-title"
|
||||
aria-describedby="alert-dialog-description"
|
||||
>
|
||||
<DialogTitle id="alert-dialog-title">Есть несохраненные изменения</DialogTitle>
|
||||
<DialogContent>
|
||||
<Typography>Вы уверены, что хотите закрыть без сохранения изменений?</Typography>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => handleConfirmClose(false)}>Отмена</Button>
|
||||
<Button onClick={() => handleConfirmClose(true)} autoFocus color="error">
|
||||
Закрыть без сохранения
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingsModal;
|
||||
|
|
@ -3,17 +3,10 @@ import React, { useState, useEffect } from "react";
|
|||
import {
|
||||
Drawer,
|
||||
List,
|
||||
Typography,
|
||||
styled,
|
||||
IconButton,
|
||||
Tooltip,
|
||||
Box,
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
Button,
|
||||
TextField
|
||||
} from "@mui/material";
|
||||
import MenuItem from "./SidebarMenuComponents/MenuItem";
|
||||
import SidebarFooter from "./SidebarMenuComponents/SidebarFooter";
|
||||
|
|
@ -27,12 +20,8 @@ const SidebarMenu = ({
|
|||
data,
|
||||
isDarkMode,
|
||||
setIsDarkMode,
|
||||
onEditItem,
|
||||
onSelectItem,
|
||||
editModalOpen,
|
||||
editingItem,
|
||||
onCloseEditModal,
|
||||
onSaveChanges
|
||||
forceRefreshMenu
|
||||
}) => {
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
const { sidebarWidth, startResizing } = useSidebarResize(290);
|
||||
|
|
@ -148,7 +137,7 @@ const SidebarMenu = ({
|
|||
item={data}
|
||||
collapsed={collapsed}
|
||||
level={0}
|
||||
onEdit={onEditItem}
|
||||
|
||||
onSelectItem={onSelectItem}
|
||||
/>
|
||||
)}
|
||||
|
|
@ -158,76 +147,15 @@ const SidebarMenu = ({
|
|||
collapsed={collapsed}
|
||||
isDarkMode={isDarkMode}
|
||||
setIsDarkMode={setIsDarkMode}
|
||||
forceRefreshMenu={forceRefreshMenu}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{!collapsed && (
|
||||
<SidebarResizer onMouseDown={startResizing} />
|
||||
)}
|
||||
</Drawer>
|
||||
|
||||
{/* Модальное окно редактирования */}
|
||||
<EditMenuItemDialog
|
||||
open={editModalOpen}
|
||||
item={editingItem}
|
||||
onClose={onCloseEditModal}
|
||||
onSave={onSaveChanges}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const EditMenuItemDialog = ({ open, item, onClose, onSave }) => {
|
||||
const [formData, setFormData] = useState(item || {});
|
||||
|
||||
useEffect(() => {
|
||||
setFormData(item || {});
|
||||
}, [item]);
|
||||
|
||||
const handleChange = (e) => {
|
||||
const { name, value } = e.target;
|
||||
setFormData(prev => ({ ...prev, [name]: value }));
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
onSave(formData);
|
||||
};
|
||||
|
||||
if (!item) return null;
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth>
|
||||
<DialogTitle>Редактирование элемента меню</DialogTitle>
|
||||
<DialogContent>
|
||||
<Box sx={{ mt: 2 }}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Название"
|
||||
name="title"
|
||||
value={formData.title || ''}
|
||||
onChange={handleChange}
|
||||
sx={{ mb: 2 }}
|
||||
/>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="ID"
|
||||
name="id"
|
||||
value={formData.id || ''}
|
||||
onChange={handleChange}
|
||||
disabled
|
||||
sx={{ mb: 2 }}
|
||||
/>
|
||||
{/* Дополнительные поля для редактирования */}
|
||||
</Box>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={onClose}>Отмена</Button>
|
||||
<Button onClick={handleSubmit} variant="contained" color="primary">
|
||||
Сохранить
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default SidebarMenu;
|
||||
|
|
@ -7,11 +7,10 @@ import {
|
|||
Collapse,
|
||||
List,
|
||||
styled,
|
||||
IconButton,
|
||||
Menu,
|
||||
MenuItem as MuiMenuItem
|
||||
} from "@mui/material";
|
||||
import { ExpandLess, ExpandMore, Folder, FolderOpen, Edit } from "@mui/icons-material";
|
||||
import { ExpandLess, ExpandMore, Folder, FolderOpen } from "@mui/icons-material";
|
||||
import StatusIndicator from "./StatusIndicator";
|
||||
|
||||
const StyledListItem = styled(ListItem)(({ theme, level }) => ({
|
||||
|
|
@ -44,11 +43,6 @@ const MenuItem = ({ item, onSelectItem, level = 0, collapsed, onEdit }) => {
|
|||
setContextMenu(null);
|
||||
};
|
||||
|
||||
const handleEditClick = () => {
|
||||
onEdit(item);
|
||||
handleCloseContextMenu();
|
||||
};
|
||||
|
||||
const handleToggle = (e) => {
|
||||
e.stopPropagation();
|
||||
setIsOpen(!isOpen);
|
||||
|
|
@ -87,19 +81,6 @@ const MenuItem = ({ item, onSelectItem, level = 0, collapsed, onEdit }) => {
|
|||
}}
|
||||
/>
|
||||
{hasChildren && (isOpen ? <ExpandLess /> : <ExpandMore />)}
|
||||
|
||||
{level > 0 && (
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleEditClick();
|
||||
}}
|
||||
sx={{ ml: 1 }}
|
||||
>
|
||||
<Edit fontSize="small" />
|
||||
</IconButton>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</StyledListItem>
|
||||
|
|
@ -114,9 +95,7 @@ const MenuItem = ({ item, onSelectItem, level = 0, collapsed, onEdit }) => {
|
|||
: undefined
|
||||
}
|
||||
>
|
||||
<MuiMenuItem onClick={handleEditClick}>
|
||||
<Edit fontSize="small" sx={{ mr: 1 }} /> Редактировать
|
||||
</MuiMenuItem>
|
||||
|
||||
</Menu>
|
||||
|
||||
{hasChildren && !collapsed && (
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import React from "react";
|
||||
// components/SidebarMenuComponents/SidebarFooter.jsx
|
||||
import React, { useState } from "react";
|
||||
import { Brightness4, Brightness7 } from "@mui/icons-material";
|
||||
import { IconButton, Tooltip } from "@mui/material";
|
||||
import {
|
||||
|
|
@ -7,8 +8,10 @@ import {
|
|||
ListItemText,
|
||||
styled,
|
||||
Switch,
|
||||
Box
|
||||
Box,
|
||||
Button
|
||||
} from "@mui/material";
|
||||
import SettingsModal from "../SettingsModal";
|
||||
|
||||
const FooterList = styled(List)(({ theme }) => ({
|
||||
backgroundColor: theme.palette.custom.sidebar,
|
||||
|
|
@ -27,52 +30,79 @@ const FooterListItem = styled(ListItem)(({ theme }) => ({
|
|||
alignItems: 'center'
|
||||
}));
|
||||
|
||||
const SidebarFooter = ({ collapsed, isDarkMode, setIsDarkMode }) => {
|
||||
return (
|
||||
<FooterList>
|
||||
{!collapsed && (
|
||||
<FooterListItem button>
|
||||
<ListItemText
|
||||
primary="Помощь"
|
||||
primaryTypographyProps={{
|
||||
color: 'custom.sidebarText',
|
||||
variant: 'body2'
|
||||
}}
|
||||
/>
|
||||
</FooterListItem>
|
||||
)}
|
||||
<FooterListItem>
|
||||
{!collapsed && (
|
||||
<ListItemText
|
||||
primary="Настройка"
|
||||
primaryTypographyProps={{
|
||||
color: 'custom.sidebarText',
|
||||
variant: 'body2'
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
const SidebarFooter = ({ collapsed, isDarkMode, setIsDarkMode, forceRefreshMenu }) => {
|
||||
const [settingsOpen, setSettingsOpen] = useState(false);
|
||||
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<Tooltip title="Переключить тему">
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => setIsDarkMode(!isDarkMode)}
|
||||
sx={{ color: 'custom.sidebarText' }}
|
||||
>
|
||||
{isDarkMode ? <Brightness4 /> : <Brightness7 />}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
{!collapsed && (
|
||||
<Switch
|
||||
checked={isDarkMode}
|
||||
onChange={() => setIsDarkMode(!isDarkMode)}
|
||||
size="small"
|
||||
const handleSettingsOpen = () => {
|
||||
setSettingsOpen(true);
|
||||
};
|
||||
|
||||
const handleSettingsClose = () => {
|
||||
setSettingsOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<FooterList>
|
||||
{!collapsed && (
|
||||
<FooterListItem button>
|
||||
<ListItemText
|
||||
primary="Помощь"
|
||||
primaryTypographyProps={{
|
||||
color: 'custom.sidebarText',
|
||||
variant: 'body2'
|
||||
}}
|
||||
/>
|
||||
</FooterListItem>
|
||||
)}
|
||||
<FooterListItem>
|
||||
{!collapsed && (
|
||||
<Button
|
||||
onClick={handleSettingsOpen}
|
||||
sx={{
|
||||
color: 'custom.sidebarText',
|
||||
textTransform: 'none',
|
||||
minWidth: 0,
|
||||
padding: 0,
|
||||
marginRight: 'auto'
|
||||
}}
|
||||
>
|
||||
<ListItemText
|
||||
primary="Настройки"
|
||||
primaryTypographyProps={{
|
||||
color: 'custom.sidebarText',
|
||||
variant: 'body2'
|
||||
}}
|
||||
/>
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
</FooterListItem>
|
||||
</FooterList>
|
||||
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<Tooltip title="Переключить тему">
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => setIsDarkMode(!isDarkMode)}
|
||||
sx={{ color: 'custom.sidebarText' }}
|
||||
>
|
||||
{isDarkMode ? <Brightness4 /> : <Brightness7 />}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
{!collapsed && (
|
||||
<Switch
|
||||
checked={isDarkMode}
|
||||
onChange={() => setIsDarkMode(!isDarkMode)}
|
||||
size="small"
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</FooterListItem>
|
||||
</FooterList>
|
||||
|
||||
<SettingsModal open={settingsOpen}
|
||||
onClose={handleSettingsClose}
|
||||
onMenuUpdate={forceRefreshMenu} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SidebarFooter;
|
||||
export default SidebarFooter;
|
||||
|
|
@ -5,16 +5,61 @@ import axios from 'axios';
|
|||
|
||||
const SidebarMenuWrapper = ({ isDarkMode, setIsDarkMode, onMenuSelect }) => {
|
||||
const [menuData, setMenuData] = useState(null);
|
||||
const [lastModified, setLastModified] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState(null);
|
||||
const [editingItem, setEditingItem] = useState(null);
|
||||
const [editModalOpen, setEditModalOpen] = useState(false);
|
||||
const [backgroundLoading, setBackgroundLoading] = useState(false);
|
||||
const [refreshTrigger, setRefreshTrigger] = useState(0);
|
||||
|
||||
|
||||
const forceRefreshMenu = () => {
|
||||
setRefreshTrigger(prev => prev + 1);
|
||||
localStorage.removeItem('menuCache'); // Очищаем кэш
|
||||
};
|
||||
|
||||
// Загружаем меню из localStorage при инициализации
|
||||
useEffect(() => {
|
||||
const loadCachedMenu = () => {
|
||||
try {
|
||||
const cached = localStorage.getItem('menuCache');
|
||||
if (cached) {
|
||||
const { data, timestamp } = JSON.parse(cached);
|
||||
setMenuData(data);
|
||||
setLastModified(timestamp);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Failed to load menu from cache', e);
|
||||
}
|
||||
};
|
||||
|
||||
loadCachedMenu();
|
||||
}, []);
|
||||
|
||||
// Основная загрузка меню
|
||||
useEffect(() => {
|
||||
const fetchMenuData = async () => {
|
||||
try {
|
||||
const response = await axios.get(`${import.meta.env.VITE_BACK_URL}/api/menu/full`);
|
||||
setMenuData(response.data); // axios хранит данные в response.data
|
||||
setLoading(true);
|
||||
const headers = lastModified ? { 'If-Modified-Since': lastModified } : {};
|
||||
|
||||
const response = await axios.get(`${import.meta.env.VITE_BACK_URL}/api/menu/full`, {
|
||||
headers,
|
||||
validateStatus: status => status === 200 || status === 304
|
||||
});
|
||||
|
||||
if (response.status === 200) {
|
||||
const newLastModified = response.headers['last-modified'];
|
||||
setMenuData(response.data);
|
||||
setLastModified(newLastModified);
|
||||
|
||||
// Сохраняем в кэш
|
||||
localStorage.setItem('menuCache', JSON.stringify({
|
||||
data: response.data,
|
||||
timestamp: newLastModified
|
||||
}));
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error fetching menu data:', err);
|
||||
setError(err.message || 'Failed to fetch menu data');
|
||||
|
|
@ -24,7 +69,41 @@ const SidebarMenuWrapper = ({ isDarkMode, setIsDarkMode, onMenuSelect }) => {
|
|||
};
|
||||
|
||||
fetchMenuData();
|
||||
}, []);
|
||||
}, [refreshTrigger]);
|
||||
|
||||
// Фоновая проверка обновлений
|
||||
useEffect(() => {
|
||||
if (!lastModified) return;
|
||||
|
||||
const checkForUpdates = async () => {
|
||||
try {
|
||||
setBackgroundLoading(true);
|
||||
const response = await axios.get(`${import.meta.env.VITE_BACK_URL}/api/menu/check-updates`, {
|
||||
headers: { 'If-Modified-Since': lastModified }
|
||||
});
|
||||
|
||||
if (response.data.hasUpdates) {
|
||||
// Если есть обновления, загружаем их в фоне
|
||||
const updateResponse = await axios.get(`${import.meta.env.VITE_BACK_URL}/api/menu/full`);
|
||||
setMenuData(updateResponse.data);
|
||||
setLastModified(updateResponse.headers['last-modified']);
|
||||
|
||||
localStorage.setItem('menuCache', JSON.stringify({
|
||||
data: updateResponse.data,
|
||||
timestamp: updateResponse.headers['last-modified']
|
||||
}));
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('Background update check failed', err);
|
||||
} finally {
|
||||
setBackgroundLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Проверяем обновления каждые 5 минут
|
||||
const interval = setInterval(checkForUpdates, 5 * 60 * 1000);
|
||||
return () => clearInterval(interval);
|
||||
}, [lastModified]);
|
||||
|
||||
const handleSaveChanges = async (updatedItem) => {
|
||||
try {
|
||||
|
|
@ -97,6 +176,7 @@ const SidebarMenuWrapper = ({ isDarkMode, setIsDarkMode, onMenuSelect }) => {
|
|||
editingItem={editingItem}
|
||||
onCloseEditModal={() => setEditModalOpen(false)}
|
||||
onSaveChanges={handleSaveChanges}
|
||||
forceRefreshMenu={forceRefreshMenu}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in New Issue