This commit is contained in:
Den Piligrim
2026-01-30 10:13:13 +03:00
parent ba6db6edd5
commit 6db895bcc7
8 changed files with 254 additions and 61 deletions
+4 -11
View File
@@ -1,17 +1,14 @@
import React from 'react';
import { Box, Container, Grid, Typography, IconButton, Link, Stack, useTheme } from '@mui/material';
import { GitHub, YouTube, Telegram, Article } from '@mui/icons-material';
import { Box, Container, Grid, IconButton, Link, Stack } from '@mui/material';
import { GitHub, YouTube, Telegram } from '@mui/icons-material';
export default function Footer() {
const theme = useTheme();
return (
<Box
component="footer"
sx={{
py: 3,
px: 2,
mt: 'auto', // Ключевой стиль для прижатия к низу
mt: 'auto',
backgroundColor: (theme) =>
theme.palette.mode === 'light'
? theme.palette.grey[200]
@@ -21,18 +18,15 @@ export default function Footer() {
<Container maxWidth={false}>
<Grid container spacing={4} justifyContent="space-between" alignItems="center">
{/* Логотип и копирайт */}
<Grid size={{ xs: 12, sm: 4 }}>
<Stack direction="row" alignItems="center" spacing={1}>
<img src="/img/logo.png" alt="Logo" width={32} height={32} style={{ marginRight: 14 }} />
</Stack>
</Grid>
{/* Ссылка на документацию */}
<Grid size={{ xs: 12, sm: 4 }} sx={{ textAlign: { xs: 'left', sm: 'center' } }}>
<Link
href="https://3dp-manager.com/docs/intro" // Ссылка на ваш репо или доку
href="https://3dp-manager.com/docs/intro"
target="_blank"
rel="noopener"
color="text.primary"
@@ -43,7 +37,6 @@ export default function Footer() {
</Link>
</Grid>
{/* Социальные иконки */}
<Grid size={{ xs: 12, sm: 4 }} sx={{ textAlign: { xs: 'left', sm: 'right' } }}>
<Stack direction="row" spacing={1} justifyContent={{ xs: 'flex-start', sm: 'flex-end' }}>
+54 -6
View File
@@ -1,10 +1,15 @@
import React, { useEffect, useState } from 'react';
import { useEffect, useState } from 'react';
import {
Box, Button, Typography, Paper, Table, TableBody, TableCell,
TableHead, TableRow, IconButton, Dialog, DialogTitle,
DialogContent, TextField, DialogActions
DialogContent, TextField, DialogActions,
FormControl,
Select,
InputAdornment,
MenuItem,
type SelectChangeEvent
} from '@mui/material';
import { Delete, Add, Link as LinkIcon, Refresh, QrCode, Share, OpenInNew, CopyAll, ContentCopy } from '@mui/icons-material';
import { Delete, Add, Link as LinkIcon, Refresh, OpenInNew, ContentCopy, Dns, Router } from '@mui/icons-material';
import api from '../api';
interface Subscription {
@@ -14,12 +19,20 @@ interface Subscription {
inbounds: any[];
}
interface Tunnel {
id: number;
name: string;
ip: string;
}
export default function SubscriptionsPage() {
const [subs, setSubs] = useState<Subscription[]>([]);
const [open, setOpen] = useState(false);
const [name, setName] = useState('');
const [tunnels, setTunnels] = useState<Tunnel[]>([]);
const [selectedServer, setSelectedServer] = useState<string>('main');
// Для модалки со ссылками
const [linksOpen, setLinksOpen] = useState(false);
const [currentLinks, setCurrentLinks] = useState<string[]>([]);
@@ -28,6 +41,8 @@ export default function SubscriptionsPage() {
const loadSubs = async () => {
const { data } = await api.get('/subscriptions');
setSubs(data);
const tunnelsRes = await api.get('/tunnels');
setTunnels(tunnelsRes.data);
};
const handleCreate = async () => {
@@ -54,16 +69,49 @@ export default function SubscriptionsPage() {
setLinksOpen(true);
};
const handleServerChange = (event: SelectChangeEvent) => {
setSelectedServer(event.target.value as string);
};
return (
<Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 3 }}>
<Typography variant="h4">Подписки</Typography>
{tunnels.length > 0 && (
<FormControl variant='standard' size="small" sx={{ minWidth: 220, justifyContent: 'center' }}>
<Select
labelId="server-select-label"
value={selectedServer}
onChange={handleServerChange}
startAdornment={
<InputAdornment position="start">
{selectedServer === 'main' ? <Dns fontSize="small"/> : <Router fontSize="small"/>}
</InputAdornment>
}
>
<MenuItem value="main">
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
<Typography variant="body2" sx={{ fontWeight: 600 }}>Основной сервер</Typography>
</Box>
</MenuItem>
{tunnels.map((t) => (
<MenuItem key={t.id} value={t.id.toString()}>
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
<Typography variant="body2" sx={{ fontWeight: 600 }}>{t.name}</Typography>
</Box>
</MenuItem>
))}
</Select>
</FormControl>
)}
<Box>
<Button startIcon={<Refresh />} onClick={loadSubs} sx={{ mr: 1 }}>Обновить</Button>
<Button variant="contained" startIcon={<Add />} onClick={() => setOpen(true)}>Создать</Button>
</Box>
</Box>
<Paper>
<Table>
<TableHead>
@@ -83,14 +131,14 @@ export default function SubscriptionsPage() {
<TableCell align="right">
<IconButton
color="primary"
onClick={() => navigator.clipboard.writeText(`http://localhost:3000/bus/${sub.uuid}`)}
onClick={() => navigator.clipboard.writeText(selectedServer === 'main' ? `http://localhost:3000/bus/${sub.uuid}` : `http://localhost:3000/bus/${sub.uuid}/${selectedServer}`)}
title="Копировать ссылку"
>
<ContentCopy />
</IconButton>
<IconButton
color="primary"
onClick={() => window.open(`http://localhost:3000/bus/${sub.uuid}`, '_blank')}
onClick={() => window.open(selectedServer === 'main' ? `http://localhost:3000/bus/${sub.uuid}` : `http://localhost:3000/bus/${sub.uuid}/${selectedServer}`, '_blank')}
title="Открыть подписку"
>
<OpenInNew />
+13 -11
View File
@@ -70,7 +70,7 @@ export default function TunnelsPage() {
return (
<Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 3 }}>
<Typography variant="h4">Редирект серверы (Туннели)</Typography>
<Typography variant="h4">Relay серверы</Typography>
<Button variant="contained" startIcon={<Add />} onClick={() => setOpen(true)}>Добавить</Button>
</Box>
@@ -101,16 +101,18 @@ export default function TunnelsPage() {
}
</TableCell>
<TableCell align="right">
<Button
startIcon={loadingId === t.id ? <CircularProgress size={20} /> : <Terminal />}
disabled={loadingId !== null} // Блокируем всё, пока идет установка
onClick={() => handleInstall(t.id)}
sx={{ mr: 1 }}
variant="outlined"
size="small"
>
{loadingId === t.id ? 'Установка...' : 'Установить'}
</Button>
{!t.isInstalled && (
<Button
startIcon={loadingId === t.id ? <CircularProgress size={20} /> : <Terminal />}
disabled={loadingId !== null}
onClick={() => handleInstall(t.id)}
sx={{ mr: 1 }}
variant="outlined"
size="small"
>
{loadingId === t.id ? 'Установка...' : 'Установить'}
</Button>
)}
<IconButton color="inherit" onClick={() => handleDelete(t.id)}>
<Delete />
</IconButton>
+21 -28
View File
@@ -1,30 +1,27 @@
import type { PaletteMode } from '@mui/material';
import { amber, deepOrange, grey } from '@mui/material/colors';
// 1. Определение цветов для Светлой темы
const lightPalette = {
primary: {
main: '#2563eb', // Насыщенный синий (Tailwind Blue 600)
main: '#2563eb',
light: '#60a5fa',
dark: '#1e40af',
},
secondary: {
main: '#7c3aed', // Фиолетовый
main: '#7c3aed',
},
background: {
default: '#f3f4f6', // Светло-серый фон (не чисто белый)
paper: '#ffffff', // Карточки белые
default: '#f3f4f6',
paper: '#ffffff',
},
text: {
primary: '#111827', // Почти черный
secondary: '#6b7280', // Серый текст
primary: '#111827',
secondary: '#6b7280',
},
};
// 2. Определение цветов для Темной темы
const darkPalette = {
primary: {
main: '#3b82f6', // Чуть светлее синий для контраста на темном
main: '#3b82f6',
light: '#60a5fa',
dark: '#1d4ed8',
},
@@ -32,16 +29,15 @@ const darkPalette = {
main: '#8b5cf6',
},
background: {
default: '#0B0F19', // Глубокий темный (Deep Space), лучше чем #121212
paper: '#111827', // Чуть светлее фона (Gray 900)
default: '#0B0F19',
paper: '#111827',
},
text: {
primary: '#f9fafb', // Почти белый
secondary: '#9ca3af', // Светло-серый
primary: '#f9fafb',
secondary: '#9ca3af',
},
};
// 3. Функция генерации настроек
export const getDesignTokens = (mode: PaletteMode) => ({
palette: {
mode,
@@ -56,15 +52,14 @@ export const getDesignTokens = (mode: PaletteMode) => ({
h5: { fontWeight: 600 },
h6: { fontWeight: 600 },
button: {
textTransform: 'none' as const, // Убираем CAPS LOCK на кнопках
textTransform: 'none' as const,
fontWeight: 600,
},
},
shape: {
borderRadius: 12, // Скругляем углы у всего (кнопки, карты)
borderRadius: 12,
},
components: {
// Кастомизация глобальных стилей (скроллбар)
MuiCssBaseline: {
styleOverrides: {
body: {
@@ -87,7 +82,6 @@ export const getDesignTokens = (mode: PaletteMode) => ({
},
},
},
// Кастомизация Кнопок
MuiButton: {
styleOverrides: {
root: {
@@ -104,11 +98,10 @@ export const getDesignTokens = (mode: PaletteMode) => ({
},
},
},
// Кастомизация Карточек (Paper)
MuiPaper: {
styleOverrides: {
root: {
backgroundImage: 'none', // Убираем осветление в темной теме (стандарт Material)
backgroundImage: 'none',
},
elevation1: {
boxShadow: mode === 'light'
@@ -118,7 +111,6 @@ export const getDesignTokens = (mode: PaletteMode) => ({
},
},
},
// Кастомизация Инпутов
MuiOutlinedInput: {
styleOverrides: {
root: {
@@ -131,12 +123,11 @@ export const getDesignTokens = (mode: PaletteMode) => ({
},
},
},
// Кастомизация AppBar (Хедера)
MuiAppBar: {
styleOverrides: {
root: {
backgroundColor: mode === 'light' ? 'rgba(255, 255, 255, 0.8)' : 'rgba(17, 24, 39, 0.8)',
backdropFilter: 'blur(8px)', // Эффект стекла
backdropFilter: 'blur(8px)',
borderBottom: `1px solid ${mode === 'light' ? '#e5e7eb' : '#374151'}`,
boxShadow: 'none',
color: mode === 'light' ? '#111827' : '#f9fafb',
@@ -144,10 +135,12 @@ export const getDesignTokens = (mode: PaletteMode) => ({
},
},
MuiTableRow: {
root: {
"&:last-child td": {
borderBottom: 0,
},
styleOverrides: {
root: {
"&:last-child td": {
borderBottom: 0,
},
}
}
}
},