added menu editor
parent
205ddc71e0
commit
34f2010cae
|
|
@ -0,0 +1,7 @@
|
|||
node_modules
|
||||
.git
|
||||
.gitignore
|
||||
Dockerfile
|
||||
.dockerignore
|
||||
dist
|
||||
npm-debug.log
|
||||
|
|
@ -0,0 +1,303 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemText,
|
||||
ListItemSecondaryAction,
|
||||
IconButton,
|
||||
TextField,
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
Button,
|
||||
Chip,
|
||||
Collapse,
|
||||
CircularProgress
|
||||
} from '@mui/material';
|
||||
import {
|
||||
Edit as EditIcon,
|
||||
Delete as DeleteIcon,
|
||||
ExpandMore as ExpandMoreIcon,
|
||||
ExpandLess as ExpandLessIcon
|
||||
} from '@mui/icons-material';
|
||||
import axios from 'axios';
|
||||
|
||||
const MenuItemComponent = ({ item, level = 0, onEdit, onDelete }) => {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const hasChildren = item.items && item.items.length > 0;
|
||||
|
||||
const handleToggle = () => {
|
||||
if (hasChildren) {
|
||||
setExpanded(!expanded);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<ListItem
|
||||
sx={{
|
||||
pl: level * 4,
|
||||
borderBottom: '1px solid',
|
||||
borderColor: 'divider'
|
||||
}}
|
||||
>
|
||||
<ListItemText
|
||||
primary={
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<Typography variant="body1">{item.title}</Typography>
|
||||
{item.isDynamic && (
|
||||
<Chip
|
||||
label="Динамический"
|
||||
size="small"
|
||||
color="info"
|
||||
variant="outlined"
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
}
|
||||
secondary={item.id}
|
||||
/>
|
||||
<ListItemSecondaryAction>
|
||||
{/* */}
|
||||
<>
|
||||
<IconButton
|
||||
edge="end"
|
||||
aria-label="edit"
|
||||
onClick={() => onEdit(item)}
|
||||
sx={{ mr: 1 }}
|
||||
>
|
||||
<EditIcon />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
edge="end"
|
||||
aria-label="delete"
|
||||
onClick={() => onDelete(item)}
|
||||
color="error"
|
||||
>
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
</>
|
||||
{hasChildren && (
|
||||
<IconButton
|
||||
edge="end"
|
||||
aria-label="expand"
|
||||
onClick={handleToggle}
|
||||
sx={{ ml: 1 }}
|
||||
>
|
||||
{expanded ? <ExpandLessIcon /> : <ExpandMoreIcon />}
|
||||
</IconButton>
|
||||
)}
|
||||
</ListItemSecondaryAction>
|
||||
</ListItem>
|
||||
{hasChildren && (
|
||||
<Collapse in={expanded} timeout="auto" unmountOnExit>
|
||||
<List component="div" disablePadding>
|
||||
{item.items.map((child) => (
|
||||
<MenuItemComponent
|
||||
key={child.id}
|
||||
item={child}
|
||||
level={level + 1}
|
||||
onEdit={onEdit}
|
||||
onDelete={onDelete}
|
||||
/>
|
||||
))}
|
||||
</List>
|
||||
</Collapse>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const EditDialog = ({ open, item, onClose, onSave }) => {
|
||||
const [title, setTitle] = useState(item?.title || '');
|
||||
|
||||
useEffect(() => {
|
||||
setTitle(item?.title || '');
|
||||
}, [item]);
|
||||
|
||||
const handleSave = () => {
|
||||
onSave(item.id, { title });
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth>
|
||||
<DialogTitle>Редактировать элемент меню</DialogTitle>
|
||||
<DialogContent>
|
||||
<TextField
|
||||
autoFocus
|
||||
margin="dense"
|
||||
label="Название"
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
sx={{ mt: 2 }}
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={onClose}>Отмена</Button>
|
||||
<Button onClick={handleSave} variant="contained">
|
||||
Сохранить
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
const MenuEditor = ({ onSave }) => {
|
||||
const [menuData, setMenuData] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState(null);
|
||||
const [editDialogOpen, setEditDialogOpen] = useState(false);
|
||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||
const [selectedItem, setSelectedItem] = useState(null);
|
||||
const [hasChanges, setHasChanges] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
fetchMenuData();
|
||||
}, []);
|
||||
|
||||
const fetchMenuData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await axios.get('/api/menu/full');
|
||||
setMenuData(response.data);
|
||||
setError(null);
|
||||
} catch (err) {
|
||||
setError('Ошибка загрузки меню');
|
||||
console.error('Error fetching menu:', err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEdit = (item) => {
|
||||
setSelectedItem(item);
|
||||
setEditDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleDelete = (item) => {
|
||||
setSelectedItem(item);
|
||||
setDeleteDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleEditSave = async (id, updates) => {
|
||||
try {
|
||||
await axios.put(`/api/menu/${id}`, updates);
|
||||
setHasChanges(true);
|
||||
fetchMenuData();
|
||||
} catch (err) {
|
||||
console.error('Error updating menu item:', err);
|
||||
alert('Ошибка при сохранении изменений');
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteConfirm = async () => {
|
||||
try {
|
||||
await axios.delete(`/api/menu/items/${selectedItem.id}`);
|
||||
setHasChanges(true);
|
||||
setDeleteDialogOpen(false);
|
||||
fetchMenuData();
|
||||
} catch (err) {
|
||||
console.error('Error deleting menu item:', err);
|
||||
alert('Ошибка при удалении элемента');
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
if (hasChanges) {
|
||||
onSave({
|
||||
hasChanges: true, saveChanges: async () => {
|
||||
// Принудительно обновляем кэш
|
||||
try {
|
||||
await axios.post('/api/menu/invalidate-cache');
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.error('Error invalidating cache:', err);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
});
|
||||
setHasChanges(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', p: 3 }}>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<Box sx={{ p: 3 }}>
|
||||
<Typography color="error">{error}</Typography>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Редактирование меню
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" paragraph>
|
||||
Вы можете редактировать названия и удалять элементы меню. Динамические элементы (помечены синим) нельзя редактировать.
|
||||
</Typography>
|
||||
|
||||
<List>
|
||||
{menuData.items.map((item) => (
|
||||
<MenuItemComponent
|
||||
key={item.id}
|
||||
item={item}
|
||||
onEdit={handleEdit}
|
||||
onDelete={handleDelete}
|
||||
/>
|
||||
))}
|
||||
</List>
|
||||
|
||||
<EditDialog
|
||||
open={editDialogOpen}
|
||||
item={selectedItem}
|
||||
onClose={() => setEditDialogOpen(false)}
|
||||
onSave={handleEditSave}
|
||||
/>
|
||||
|
||||
<Dialog
|
||||
open={deleteDialogOpen}
|
||||
onClose={() => setDeleteDialogOpen(false)}
|
||||
>
|
||||
<DialogTitle>Подтверждение удаления</DialogTitle>
|
||||
<DialogContent>
|
||||
<Typography>
|
||||
Вы уверены, что хотите удалить элемент "{selectedItem?.title}"?
|
||||
</Typography>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setDeleteDialogOpen(false)}>Отмена</Button>
|
||||
<Button onClick={handleDeleteConfirm} color="error" variant="contained">
|
||||
Удалить
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
<Box sx={{ mt: 2, display: 'flex', justifyContent: 'flex-end' }}>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={handleSave}
|
||||
disabled={!hasChanges}
|
||||
>
|
||||
Применить изменения
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default MenuEditor;
|
||||
|
|
@ -21,6 +21,7 @@ import CloseIcon from '@mui/icons-material/Close';
|
|||
import SaveIcon from '@mui/icons-material/Save';
|
||||
import MetricRangeEditor from './SettingsComponents/MetricRangeEditor';
|
||||
import UserManagement from './SettingsComponents/UserManagement';
|
||||
import MenuEditor from './SettingsComponents/MenuEditor'
|
||||
|
||||
const Transition = React.forwardRef(function Transition(props, ref) {
|
||||
return <Slide direction="up" ref={ref} {...props} />;
|
||||
|
|
@ -64,6 +65,10 @@ const SettingsModal = ({ open, onClose, onMenuUpdate }) => {
|
|||
hasChanges: false,
|
||||
save: () => { }
|
||||
});
|
||||
const [menuEditorState, setMenuEditorState] = useState({
|
||||
hasChanges: false,
|
||||
save: () => Promise.resolve(true)
|
||||
});
|
||||
|
||||
const handleTabChange = (event, newValue) => {
|
||||
if (hasChanges) {
|
||||
|
|
@ -73,12 +78,22 @@ const SettingsModal = ({ open, onClose, onMenuUpdate }) => {
|
|||
}
|
||||
};
|
||||
|
||||
const handleMenuEditorChange = ({ hasChanges, saveChanges }) => {
|
||||
setMenuEditorState({ hasChanges, save: saveChanges });
|
||||
setHasChanges(hasChanges);
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
setIsSaving(true);
|
||||
try {
|
||||
let success = true;
|
||||
|
||||
if (tabValue === 0 && menuEditorState.hasChanges) {
|
||||
success = await menuEditorState.save();
|
||||
}
|
||||
|
||||
if (tabValue === 1 && metricEditorState.hasChanges) {
|
||||
success = await metricEditorState.save();
|
||||
success = success && await metricEditorState.save();
|
||||
}
|
||||
|
||||
if (success) {
|
||||
|
|
@ -113,7 +128,6 @@ const SettingsModal = ({ open, onClose, onMenuUpdate }) => {
|
|||
}
|
||||
};
|
||||
|
||||
// Пример обработчика изменений
|
||||
const handleSettingChange = () => {
|
||||
setHasChanges(true);
|
||||
};
|
||||
|
|
@ -149,14 +163,13 @@ const SettingsModal = ({ open, onClose, onMenuUpdate }) => {
|
|||
<Tab label="Меню" id="settings-tab-0" aria-controls="settings-tabpanel-0" />
|
||||
<Tab label="Границы метрик" id="settings-tab-1" aria-controls="settings-tabpanel-1" />
|
||||
<Tab label="Управление пользователями" id="settings-tab-2" aria-controls="settings-tabpanel-2" />
|
||||
{/* Добавляйте новые вкладки здесь */}
|
||||
{/* Добавить новые вкладки здесь */}
|
||||
</Tabs>
|
||||
</Box>
|
||||
|
||||
<DialogContent dividers>
|
||||
<TabPanel value={tabValue} index={0}>
|
||||
<Typography variant="h6">Настройки меню</Typography>
|
||||
{/* Добавьте содержимое для вкладки меню */}
|
||||
<MenuEditor onSave={handleMenuEditorChange} />
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel value={tabValue} index={1}>
|
||||
|
|
|
|||
|
|
@ -14,12 +14,12 @@ export default defineConfig({
|
|||
rewrite: (path) => path.replace(/^\/ai-api/, ''),
|
||||
},
|
||||
'/metrics-ws': {
|
||||
target: 'ws://localhost:3001',
|
||||
target: 'ws://192.168.2.39:3001',
|
||||
ws: true,
|
||||
changeOrigin: true,
|
||||
},
|
||||
'/api': {
|
||||
target: 'http://localhost:3000',
|
||||
target: 'http://192.168.2.39:3000',
|
||||
changeOrigin: true,
|
||||
bypass(req, res, options) {
|
||||
console.log('Proxying request:', req.url);
|
||||
|
|
@ -27,4 +27,4 @@ export default defineConfig({
|
|||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue