update
This commit is contained in:
@@ -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' }}>
|
||||
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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
@@ -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,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user