added menu editor

pull/59/head
DmitriyA 2025-08-28 09:20:42 -04:00
parent 205ddc71e0
commit 34f2010cae
4 changed files with 331 additions and 8 deletions

7
.dockerignore Normal file
View File

@ -0,0 +1,7 @@
node_modules
.git
.gitignore
Dockerfile
.dockerignore
dist
npm-debug.log

View File

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

View File

@ -21,6 +21,7 @@ import CloseIcon from '@mui/icons-material/Close';
import SaveIcon from '@mui/icons-material/Save'; import SaveIcon from '@mui/icons-material/Save';
import MetricRangeEditor from './SettingsComponents/MetricRangeEditor'; import MetricRangeEditor from './SettingsComponents/MetricRangeEditor';
import UserManagement from './SettingsComponents/UserManagement'; import UserManagement from './SettingsComponents/UserManagement';
import MenuEditor from './SettingsComponents/MenuEditor'
const Transition = React.forwardRef(function Transition(props, ref) { const Transition = React.forwardRef(function Transition(props, ref) {
return <Slide direction="up" ref={ref} {...props} />; return <Slide direction="up" ref={ref} {...props} />;
@ -64,6 +65,10 @@ const SettingsModal = ({ open, onClose, onMenuUpdate }) => {
hasChanges: false, hasChanges: false,
save: () => { } save: () => { }
}); });
const [menuEditorState, setMenuEditorState] = useState({
hasChanges: false,
save: () => Promise.resolve(true)
});
const handleTabChange = (event, newValue) => { const handleTabChange = (event, newValue) => {
if (hasChanges) { if (hasChanges) {
@ -73,12 +78,22 @@ const SettingsModal = ({ open, onClose, onMenuUpdate }) => {
} }
}; };
const handleMenuEditorChange = ({ hasChanges, saveChanges }) => {
setMenuEditorState({ hasChanges, save: saveChanges });
setHasChanges(hasChanges);
};
const handleSave = async () => { const handleSave = async () => {
setIsSaving(true); setIsSaving(true);
try { try {
let success = true; let success = true;
if (tabValue === 0 && menuEditorState.hasChanges) {
success = await menuEditorState.save();
}
if (tabValue === 1 && metricEditorState.hasChanges) { if (tabValue === 1 && metricEditorState.hasChanges) {
success = await metricEditorState.save(); success = success && await metricEditorState.save();
} }
if (success) { if (success) {
@ -113,7 +128,6 @@ const SettingsModal = ({ open, onClose, onMenuUpdate }) => {
} }
}; };
// Пример обработчика изменений
const handleSettingChange = () => { const handleSettingChange = () => {
setHasChanges(true); 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-0" aria-controls="settings-tabpanel-0" />
<Tab label="Границы метрик" id="settings-tab-1" aria-controls="settings-tabpanel-1" /> <Tab label="Границы метрик" id="settings-tab-1" aria-controls="settings-tabpanel-1" />
<Tab label="Управление пользователями" id="settings-tab-2" aria-controls="settings-tabpanel-2" /> <Tab label="Управление пользователями" id="settings-tab-2" aria-controls="settings-tabpanel-2" />
{/* Добавляйте новые вкладки здесь */} {/* Добавить новые вкладки здесь */}
</Tabs> </Tabs>
</Box> </Box>
<DialogContent dividers> <DialogContent dividers>
<TabPanel value={tabValue} index={0}> <TabPanel value={tabValue} index={0}>
<Typography variant="h6">Настройки меню</Typography> <MenuEditor onSave={handleMenuEditorChange} />
{/* Добавьте содержимое для вкладки меню */}
</TabPanel> </TabPanel>
<TabPanel value={tabValue} index={1}> <TabPanel value={tabValue} index={1}>

View File

@ -14,12 +14,12 @@ export default defineConfig({
rewrite: (path) => path.replace(/^\/ai-api/, ''), rewrite: (path) => path.replace(/^\/ai-api/, ''),
}, },
'/metrics-ws': { '/metrics-ws': {
target: 'ws://localhost:3001', target: 'ws://192.168.2.39:3001',
ws: true, ws: true,
changeOrigin: true, changeOrigin: true,
}, },
'/api': { '/api': {
target: 'http://localhost:3000', target: 'http://192.168.2.39:3000',
changeOrigin: true, changeOrigin: true,
bypass(req, res, options) { bypass(req, res, options) {
console.log('Proxying request:', req.url); console.log('Proxying request:', req.url);
@ -27,4 +27,4 @@ export default defineConfig({
} }
} }
} }
}); });