mirror of
https://github.com/Jerryplusy/AI-powered-switches.git
synced 2025-07-04 13:19:20 +00:00
重写通知模块,增加两个页面
This commit is contained in:
parent
d6787a11dd
commit
3070c676f8
@ -2,6 +2,7 @@ import { BrowserRouter, Routes, Route } from 'react-router-dom';
|
|||||||
import AppShell from '@/components/system/layout/AppShell';
|
import AppShell from '@/components/system/layout/AppShell';
|
||||||
import buildRoutes from '@/constants/routes/routes';
|
import buildRoutes from '@/constants/routes/routes';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
|
import { Toaster } from '@/components/ui/toaster';
|
||||||
|
|
||||||
const App = () => {
|
const App = () => {
|
||||||
const isProd = process.env.NODE_ENV === 'production';
|
const isProd = process.env.NODE_ENV === 'production';
|
||||||
@ -13,6 +14,7 @@ const App = () => {
|
|||||||
{buildRoutes()}
|
{buildRoutes()}
|
||||||
</Route>
|
</Route>
|
||||||
</Routes>
|
</Routes>
|
||||||
|
<Toaster />
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useLocation, useNavigate } from 'react-router-dom';
|
import { useLocation, useNavigate } from 'react-router-dom';
|
||||||
import { AnimatePresence, motion } from 'framer-motion';
|
import { AnimatePresence, motion } from 'framer-motion';
|
||||||
import { Box, Button, HStack } from '@chakra-ui/react';
|
import { Box, Button, HStack, Switch, Text } from '@chakra-ui/react';
|
||||||
import web from '@/resources/icon/pages/weclome/web.svg';
|
import web from '@/resources/icon/pages/weclome/web.svg';
|
||||||
import githubIcon from '@/resources/icon/pages/weclome/github.svg';
|
import githubIcon from '@/resources/icon/pages/weclome/github.svg';
|
||||||
import FadeInWrapper from '@/components/system/layout/FadeInWrapper';
|
import FadeInWrapper from '@/components/system/layout/FadeInWrapper';
|
||||||
import MotionCard from '@/components/ui/MotionCard';
|
import MotionCard from '@/components/ui/MotionCard';
|
||||||
|
import ConfigTool from '@/libs/config/ConfigTool';
|
||||||
|
|
||||||
const navItems = [
|
const navItems = [
|
||||||
{ label: '面板', path: '/dashboard' },
|
{ label: '面板', path: '/dashboard' },
|
||||||
@ -25,6 +26,7 @@ const GithubTransitionCard = () => {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const isDashboard = pathname.startsWith('/dashboard');
|
const isDashboard = pathname.startsWith('/dashboard');
|
||||||
const [showNavButtons, setShowNavButtons] = useState(false);
|
const [showNavButtons, setShowNavButtons] = useState(false);
|
||||||
|
const [testMode, setTestMode] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setShowNavButtons(false);
|
setShowNavButtons(false);
|
||||||
@ -33,6 +35,19 @@ const GithubTransitionCard = () => {
|
|||||||
}, 400);
|
}, 400);
|
||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
}, [isDashboard]);
|
}, [isDashboard]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const config = ConfigTool.load();
|
||||||
|
setTestMode(config.testMode);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleTestModeToggle = (checked) => {
|
||||||
|
console.log(`切换测试模式..`);
|
||||||
|
setTestMode(checked);
|
||||||
|
const config = ConfigTool.load();
|
||||||
|
ConfigTool.save({ ...config, testMode: checked });
|
||||||
|
};
|
||||||
|
|
||||||
const MotionBox = motion(Box);
|
const MotionBox = motion(Box);
|
||||||
return (
|
return (
|
||||||
<AnimatePresence mode={'wait'}>
|
<AnimatePresence mode={'wait'}>
|
||||||
@ -71,9 +86,10 @@ const GithubTransitionCard = () => {
|
|||||||
w={'100%'}
|
w={'100%'}
|
||||||
px={isDashboard ? 4 : 3}
|
px={isDashboard ? 4 : 3}
|
||||||
py={isDashboard ? 3 : 2}
|
py={isDashboard ? 3 : 2}
|
||||||
noHover={isDashboard}
|
disableHover={isDashboard}
|
||||||
>
|
>
|
||||||
{isDashboard && showNavButtons && (
|
{isDashboard && showNavButtons && (
|
||||||
|
<>
|
||||||
<HStack spacing={4} ml={'auto'}>
|
<HStack spacing={4} ml={'auto'}>
|
||||||
{navItems.map((item) => (
|
{navItems.map((item) => (
|
||||||
<Button
|
<Button
|
||||||
@ -93,7 +109,25 @@ const GithubTransitionCard = () => {
|
|||||||
{item.label}
|
{item.label}
|
||||||
</Button>
|
</Button>
|
||||||
))}
|
))}
|
||||||
|
<HStack spacing={4} ml={'auto'}>
|
||||||
|
<Text fontSize={'sm'} color={'white'}>
|
||||||
|
{'测试模式'}
|
||||||
|
</Text>
|
||||||
|
<Switch.Root
|
||||||
|
size={'sm'}
|
||||||
|
checked={testMode}
|
||||||
|
onCheckedChange={(details) => handleTestModeToggle(details.checked)}
|
||||||
|
colorPalette={'teal'}
|
||||||
|
zIndex={100}
|
||||||
|
>
|
||||||
|
<Switch.HiddenInput />
|
||||||
|
<Switch.Control>
|
||||||
|
<Switch.Thumb />
|
||||||
|
</Switch.Control>
|
||||||
|
</Switch.Root>
|
||||||
</HStack>
|
</HStack>
|
||||||
|
</HStack>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</MotionCard>
|
</MotionCard>
|
||||||
</FadeInWrapper>
|
</FadeInWrapper>
|
||||||
@ -101,5 +135,5 @@ const GithubTransitionCard = () => {
|
|||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
// TODO 解决组件重复渲染问题
|
||||||
export default GithubTransitionCard;
|
export default GithubTransitionCard;
|
||||||
|
@ -2,6 +2,8 @@ import { Route } from 'react-router-dom';
|
|||||||
import Welcome from '@/pages/Welcome';
|
import Welcome from '@/pages/Welcome';
|
||||||
import Dashboard from '@/pages/Dashboard';
|
import Dashboard from '@/pages/Dashboard';
|
||||||
import ScanPage from '@/pages/ScanPage';
|
import ScanPage from '@/pages/ScanPage';
|
||||||
|
import DevicesPage from '@/pages/DevicesPage';
|
||||||
|
import ConfigPage from '@/pages/ConfigPage';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 路由
|
* 路由
|
||||||
@ -11,6 +13,8 @@ const routeList = [
|
|||||||
{ path: '/', element: <Welcome /> },
|
{ path: '/', element: <Welcome /> },
|
||||||
{ path: '/dashboard', element: <Dashboard /> },
|
{ path: '/dashboard', element: <Dashboard /> },
|
||||||
{ path: '/dashboard/scan', element: <ScanPage /> },
|
{ path: '/dashboard/scan', element: <ScanPage /> },
|
||||||
|
{ path: '/dashboard/devices', element: <DevicesPage /> },
|
||||||
|
{ path: '/dashboard/config', element: <ConfigPage /> },
|
||||||
];
|
];
|
||||||
|
|
||||||
const buildRoutes = () =>
|
const buildRoutes = () =>
|
||||||
|
@ -2,13 +2,10 @@ import React from 'react';
|
|||||||
import ReactDOM from 'react-dom/client';
|
import ReactDOM from 'react-dom/client';
|
||||||
import App from '@/App';
|
import App from '@/App';
|
||||||
import { Provider } from '@/components/ui/provider';
|
import { Provider } from '@/components/ui/provider';
|
||||||
import { NotificationProvider } from '@/libs/system/Notification';
|
|
||||||
|
|
||||||
const root = ReactDOM.createRoot(document.getElementById('root'));
|
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||||
root.render(
|
root.render(
|
||||||
<Provider>
|
<Provider>
|
||||||
<NotificationProvider>
|
|
||||||
<App />
|
<App />
|
||||||
</NotificationProvider>
|
|
||||||
</Provider>
|
</Provider>
|
||||||
);
|
);
|
||||||
|
21
src/frontend/src/libs/common.js
Normal file
21
src/frontend/src/libs/common.js
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
const Common = {
|
||||||
|
/**
|
||||||
|
* 睡眠指定毫秒
|
||||||
|
* @param {number} ms - 毫秒数,必须是非负数
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async sleep(ms) {
|
||||||
|
if (!Number.isFinite(ms)) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
if (ms === 0) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const timer = setTimeout(resolve, ms);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Common;
|
@ -1,18 +1,26 @@
|
|||||||
const CONFIG_KEY = 'app_config';
|
const CONFIG_KEY = 'app_config';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 默认配置
|
||||||
|
* @type {{backendUrl: string, authKey: string, testMode: boolean, stats: {totalDevices: number, onlineDevices: number, lastScan: string}, devices: *[]}}
|
||||||
|
*/
|
||||||
export const defaultConfig = {
|
export const defaultConfig = {
|
||||||
backendUrl: '',
|
backendUrl: '',
|
||||||
authKey: '',
|
authKey: '',
|
||||||
|
testMode: false,
|
||||||
|
stats: {
|
||||||
|
totalDevices: 0,
|
||||||
|
onlineDevices: 0,
|
||||||
|
lastScan: '',
|
||||||
|
},
|
||||||
|
devices: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 浏览器环境配置操作工具
|
* 配置管理工具
|
||||||
|
* @type {{load: ((function(): (any))|*), save: ((function(*): (boolean|undefined))|*), clear: ((function(): (boolean|undefined))|*), getConfigPath: (function(): string), isStorageAvailable: ((function(): (boolean|undefined))|*), getStats: (function(): *)}}
|
||||||
*/
|
*/
|
||||||
const ConfigTool = {
|
const ConfigTool = {
|
||||||
/**
|
|
||||||
* 从本地存储读取配置
|
|
||||||
* @returns {{backendUrl: string, authKey: string}}
|
|
||||||
*/
|
|
||||||
load: () => {
|
load: () => {
|
||||||
try {
|
try {
|
||||||
const stored = localStorage.getItem(CONFIG_KEY);
|
const stored = localStorage.getItem(CONFIG_KEY);
|
||||||
@ -27,15 +35,13 @@ const ConfigTool = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 保存配置到本地存储
|
* 保存配置
|
||||||
* @param {Object} config - 要保存的配置对象
|
* @param config
|
||||||
* @returns {boolean} 是否保存成功
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
save: (config) => {
|
save: (config) => {
|
||||||
try {
|
try {
|
||||||
localStorage.setItem(CONFIG_KEY, JSON.stringify(config));
|
localStorage.setItem(CONFIG_KEY, JSON.stringify(config));
|
||||||
//console.log(`正在保存配置:${JSON.stringify(config)}`);
|
|
||||||
//console.log(`测试读取配置:${JSON.stringify(ConfigTool.load())}`);
|
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('保存配置失败:', e);
|
console.error('保存配置失败:', e);
|
||||||
@ -48,7 +54,7 @@ const ConfigTool = {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 清除配置
|
* 清除配置
|
||||||
* @returns {boolean} 是否清除成功
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
clear: () => {
|
clear: () => {
|
||||||
try {
|
try {
|
||||||
@ -60,16 +66,8 @@ const ConfigTool = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取存储标识(浏览器环境返回固定值)
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
getConfigPath: () => `localStorage:${CONFIG_KEY}`,
|
getConfigPath: () => `localStorage:${CONFIG_KEY}`,
|
||||||
|
|
||||||
/**
|
|
||||||
* 检查存储是否可用
|
|
||||||
* @returns {boolean}
|
|
||||||
*/
|
|
||||||
isStorageAvailable: () => {
|
isStorageAvailable: () => {
|
||||||
try {
|
try {
|
||||||
const testKey = 'test';
|
const testKey = 'test';
|
||||||
@ -80,6 +78,11 @@ const ConfigTool = {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getStats: () => {
|
||||||
|
const config = ConfigTool.load();
|
||||||
|
return config.stats || { totalDevices: 0, onlineDevices: 0, lastScan: '' };
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ConfigTool;
|
export default ConfigTool;
|
||||||
|
@ -1,114 +1,99 @@
|
|||||||
import React, { createContext, useContext, useState, useCallback } from 'react';
|
import { toaster } from '@/components/ui/toaster';
|
||||||
import { Box, Button, Icon, Text } from '@chakra-ui/react';
|
import {
|
||||||
import { AnimatePresence, motion } from 'framer-motion';
|
AiOutlineInfoCircle,
|
||||||
import { AiOutlineInfoCircle, AiFillWarning } from 'react-icons/ai';
|
AiFillWarning,
|
||||||
|
AiFillCheckCircle,
|
||||||
|
AiFillExclamationCircle,
|
||||||
|
} from 'react-icons/ai';
|
||||||
|
|
||||||
const NotificationContext = createContext(null);
|
const surfaceStyle = {
|
||||||
|
borderRadius: '1rem',
|
||||||
/**
|
border: '1px solid rgba(255,255,255,0.3)',
|
||||||
* 通知hook
|
backdropFilter: 'blur(10px)',
|
||||||
* @returns {null}
|
background: 'rgba(255, 255, 255, 0.1)',
|
||||||
*/
|
color: 'white',
|
||||||
export const useNotification = () => useContext(NotificationContext);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 通知根组件
|
|
||||||
* @param children
|
|
||||||
* @returns {JSX.Element}
|
|
||||||
* @constructor
|
|
||||||
*/
|
|
||||||
export const NotificationProvider = ({ children }) => {
|
|
||||||
const [notifications, setNotifications] = useState([]);
|
|
||||||
|
|
||||||
const addNotification = useCallback((notification) => {
|
|
||||||
const id = Date.now() + Math.random();
|
|
||||||
setNotifications((prev) => [...prev, { ...notification, id }]);
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
setNotifications((prev) => prev.filter((n) => n.id !== id));
|
|
||||||
}, 3000);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const notify = {
|
|
||||||
info: ({ title, description, button }) =>
|
|
||||||
addNotification({
|
|
||||||
type: 'info',
|
|
||||||
title,
|
|
||||||
description,
|
|
||||||
button,
|
|
||||||
}),
|
|
||||||
error: ({ title, description, button }) =>
|
|
||||||
addNotification({
|
|
||||||
type: 'error',
|
|
||||||
title,
|
|
||||||
description,
|
|
||||||
button,
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
const MotionBox = motion(Box);
|
|
||||||
|
|
||||||
// TODO 弹窗颜色问题及重新渲染问题
|
|
||||||
return (
|
|
||||||
<NotificationContext.Provider value={notify}>
|
|
||||||
{children}
|
|
||||||
<Box
|
|
||||||
position={'fixed'}
|
|
||||||
bottom={4}
|
|
||||||
right={4}
|
|
||||||
zIndex={9999}
|
|
||||||
display={'flex'}
|
|
||||||
flexDirection={'column'}
|
|
||||||
alignItems={'flex-end'}
|
|
||||||
>
|
|
||||||
<AnimatePresence>
|
|
||||||
{notifications.map((item) => (
|
|
||||||
<MotionBox
|
|
||||||
key={item.id}
|
|
||||||
layout
|
|
||||||
initial={{ opacity: 0, x: 200 }}
|
|
||||||
animate={{ opacity: 1, x: 0 }}
|
|
||||||
exit={{ opacity: 0, x: 200 }}
|
|
||||||
transition={{ type: 'spring', stiffness: 500, damping: 30 }}
|
|
||||||
mb={3}
|
|
||||||
width={'320px'}
|
|
||||||
>
|
|
||||||
<Box
|
|
||||||
bg={item.type === 'error' ? '' : ''}
|
|
||||||
color={'white'}
|
|
||||||
borderRadius={'xl'}
|
|
||||||
px={5}
|
|
||||||
py={4}
|
|
||||||
boxShadow={'2xl'}
|
|
||||||
border={'1px solid rgba(255, 255, 255, 0.4)'}
|
|
||||||
backdropFilter={'blur(8px)'}
|
|
||||||
>
|
|
||||||
<Box display={'flex'} alignItems={'center'}>
|
|
||||||
<Icon
|
|
||||||
as={item.type === 'error' ? AiFillWarning : AiOutlineInfoCircle}
|
|
||||||
boxSize={6}
|
|
||||||
mr={3}
|
|
||||||
/>
|
|
||||||
<Text fontSize={'lg'} fontWeight={'bold'}>
|
|
||||||
{item.title}
|
|
||||||
</Text>
|
|
||||||
</Box>
|
|
||||||
<Text mt={2}>{item.description}</Text>
|
|
||||||
{item.button && (
|
|
||||||
<Button
|
|
||||||
colorPalette={'whiteAlpha'}
|
|
||||||
size={'sm'}
|
|
||||||
mt={3}
|
|
||||||
onClick={item.button.onClick}
|
|
||||||
>
|
|
||||||
{item.button.label}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
</MotionBox>
|
|
||||||
))}
|
|
||||||
</AnimatePresence>
|
|
||||||
</Box>
|
|
||||||
</NotificationContext.Provider>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通用通知组件
|
||||||
|
* @type {{info({title: *, description: *, button: *}): void, success({title: *, description: *, button: *}): void, warn({title: *, description: *, button: *}): void, error({title: *, description: *, button: *}): void}}
|
||||||
|
*/
|
||||||
|
const Notification = {
|
||||||
|
/**
|
||||||
|
* 信息
|
||||||
|
* @param title 标题
|
||||||
|
* @param description 描述
|
||||||
|
* @param button 按钮
|
||||||
|
*/
|
||||||
|
info({ title, description, button }) {
|
||||||
|
toaster.create({
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
type: 'info',
|
||||||
|
duration: 3000,
|
||||||
|
icon: <AiOutlineInfoCircle size={24} />,
|
||||||
|
actionLabel: button?.label,
|
||||||
|
action: button?.onClick,
|
||||||
|
style: surfaceStyle,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 成功信息
|
||||||
|
* @param title 标题
|
||||||
|
* @param description 描述
|
||||||
|
* @param button 按钮
|
||||||
|
*/
|
||||||
|
success({ title, description, button }) {
|
||||||
|
toaster.create({
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
type: 'success',
|
||||||
|
duration: 3000,
|
||||||
|
icon: <AiFillCheckCircle size={24} />,
|
||||||
|
actionLabel: button?.label,
|
||||||
|
action: button?.onClick,
|
||||||
|
style: surfaceStyle,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 警告
|
||||||
|
* @param title 标题
|
||||||
|
* @param description 描述
|
||||||
|
* @param button 按钮
|
||||||
|
*/
|
||||||
|
warn({ title, description, button }) {
|
||||||
|
toaster.create({
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
type: 'warning',
|
||||||
|
duration: 3000,
|
||||||
|
icon: <AiFillExclamationCircle size={24} />,
|
||||||
|
actionLabel: button?.label,
|
||||||
|
action: button?.onClick,
|
||||||
|
style: surfaceStyle,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 错误提示
|
||||||
|
* @param title 标题
|
||||||
|
* @param description 描述
|
||||||
|
* @param button 按钮
|
||||||
|
*/
|
||||||
|
error({ title, description, button }) {
|
||||||
|
toaster.create({
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
type: 'error',
|
||||||
|
duration: 3000,
|
||||||
|
icon: <AiFillWarning size={24} />,
|
||||||
|
actionLabel: button?.label,
|
||||||
|
action: button?.onClick,
|
||||||
|
style: surfaceStyle,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Notification;
|
||||||
|
114
src/frontend/src/pages/ConfigPage.jsx
Normal file
114
src/frontend/src/pages/ConfigPage.jsx
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { Box, VStack, Heading, Textarea, Button, Select, Text } from '@chakra-ui/react';
|
||||||
|
import DocumentTitle from '@/components/system/pages/DocumentTitle';
|
||||||
|
import PageContainer from '@/components/system/PageContainer';
|
||||||
|
import DashboardBackground from '@/components/system/pages/DashboardBackground';
|
||||||
|
import FadeInWrapper from '@/components/system/layout/FadeInWrapper';
|
||||||
|
import ConfigTool from '@/libs/config/ConfigTool';
|
||||||
|
import { api } from '@/services/api/api';
|
||||||
|
import Notification from '@/libs/system/Notification';
|
||||||
|
|
||||||
|
const ConfigPage = () => {
|
||||||
|
const [devices, setDevices] = useState([]);
|
||||||
|
const [selectedDevice, setSelectedDevice] = useState('');
|
||||||
|
const [inputText, setInputText] = useState('');
|
||||||
|
const [parsedConfig, setParsedConfig] = useState('');
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const config = ConfigTool.load();
|
||||||
|
setDevices(config.devices || []);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleParse = async () => {
|
||||||
|
if (!selectedDevice || !inputText) {
|
||||||
|
Notification.error({ title: '请选择设备并输入配置指令' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const res = await api.parseCommand(inputText);
|
||||||
|
setParsedConfig(res.config);
|
||||||
|
} catch (e) {
|
||||||
|
Notification.error({ title: '配置解析失败' });
|
||||||
|
}
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleApply = async () => {
|
||||||
|
if (!parsedConfig) return;
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
await api.applyConfig(selectedDevice, parsedConfig);
|
||||||
|
Notification.success({ title: '配置已成功应用!' });
|
||||||
|
} catch (e) {
|
||||||
|
Notification.error({ title: '配置应用失败' });
|
||||||
|
}
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DocumentTitle title={'交换机配置'}>
|
||||||
|
<DashboardBackground />
|
||||||
|
<PageContainer>
|
||||||
|
<FadeInWrapper delay={0.3} yOffset={-5}>
|
||||||
|
<VStack spacing={8} align={'stretch'}>
|
||||||
|
<Heading fontSize={'2xl'} color={'teal.300'}>
|
||||||
|
{'交换机配置中心'}
|
||||||
|
</Heading>
|
||||||
|
|
||||||
|
<Select
|
||||||
|
placeholder={'请选择交换机设备'}
|
||||||
|
value={selectedDevice}
|
||||||
|
onChange={(e) => setSelectedDevice(e.target.value)}
|
||||||
|
bg={'whiteAlpha.200'}
|
||||||
|
>
|
||||||
|
{devices.map((device) => (
|
||||||
|
<option key={device.ip} value={device.ip}>
|
||||||
|
{device.name} ({device.ip})
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
|
||||||
|
<Textarea
|
||||||
|
rows={6}
|
||||||
|
placeholder={'输入自然语言配置指令...'}
|
||||||
|
value={inputText}
|
||||||
|
onChange={(e) => setInputText(e.target.value)}
|
||||||
|
bg={'whiteAlpha.200'}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
colorPalette={'teal'}
|
||||||
|
variant={'solid'}
|
||||||
|
isLoading={loading}
|
||||||
|
onClick={handleParse}
|
||||||
|
>
|
||||||
|
{'解析配置'}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{parsedConfig && (
|
||||||
|
<Box
|
||||||
|
p={4}
|
||||||
|
bg={'whiteAlpha.100'}
|
||||||
|
borderRadius={'xl'}
|
||||||
|
border={'1px solid'}
|
||||||
|
borderColor={'whiteAlpha.300'}
|
||||||
|
>
|
||||||
|
<Text fontWeight={'bold'} mb={2}>
|
||||||
|
{'已生成配置:'}
|
||||||
|
</Text>
|
||||||
|
<Textarea value={parsedConfig} rows={8} readOnly />
|
||||||
|
<Button mt={4} colorPalette={'teal'} onClick={handleApply}>
|
||||||
|
{'应用到交换机'}
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</VStack>
|
||||||
|
</FadeInWrapper>
|
||||||
|
</PageContainer>
|
||||||
|
</DocumentTitle>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ConfigPage;
|
@ -6,7 +6,9 @@ import DashboardBackground from '@/components/system/pages/DashboardBackground';
|
|||||||
import FadeInWrapper from '@/components/system/layout/FadeInWrapper';
|
import FadeInWrapper from '@/components/system/layout/FadeInWrapper';
|
||||||
import FeatureCard from '@/components/pages/dashboard/FeatureCard';
|
import FeatureCard from '@/components/pages/dashboard/FeatureCard';
|
||||||
import StatCard from '@/components/pages/dashboard/StatCard';
|
import StatCard from '@/components/pages/dashboard/StatCard';
|
||||||
import { useNotification } from '@/libs/system/Notification';
|
import api from '@/services/api/api';
|
||||||
|
import ConfigTool from '@/libs/config/ConfigTool';
|
||||||
|
import Notification from '@/libs/system/Notification';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 控制台
|
* 控制台
|
||||||
@ -21,24 +23,24 @@ const Dashboard = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const [networkStatus, setNetworkStatus] = useState('loading'); // loading | ok | fail
|
const [networkStatus, setNetworkStatus] = useState('loading'); // loading | ok | fail
|
||||||
const notify = useNotification();
|
|
||||||
|
|
||||||
const checkBackend = useCallback(async () => {
|
const checkBackend = useCallback(async () => {
|
||||||
setNetworkStatus('loading');
|
setNetworkStatus('loading');
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/test');
|
const res = await api.test();
|
||||||
if (res.ok) {
|
if (res) {
|
||||||
setNetworkStatus('ok');
|
setNetworkStatus('ok');
|
||||||
notify.info({ title: '成功连接至后端服务!' });
|
console.log(JSON.stringify(res));
|
||||||
|
Notification.info({ title: '成功连接至后端服务!' });
|
||||||
} else {
|
} else {
|
||||||
setNetworkStatus('fail');
|
setNetworkStatus('fail');
|
||||||
notify.error({ title: '后端服务响应异常!' });
|
Notification.error({ title: '后端服务响应异常!' });
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setNetworkStatus('fail');
|
setNetworkStatus('fail');
|
||||||
notify.error({ title: '无法连接到后端服务!' });
|
Notification.error({ title: '无法连接到后端服务!' });
|
||||||
}
|
}
|
||||||
}, [notify]);
|
}, [Notification]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
@ -47,6 +49,14 @@ const Dashboard = () => {
|
|||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
}, [checkBackend]);
|
}, [checkBackend]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const loadStats = () => {
|
||||||
|
const stats = ConfigTool.getStats();
|
||||||
|
setStats(stats);
|
||||||
|
};
|
||||||
|
loadStats();
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DocumentTitle title={'控制台'}>
|
<DocumentTitle title={'控制台'}>
|
||||||
<DashboardBackground />
|
<DashboardBackground />
|
||||||
@ -97,29 +107,17 @@ const Dashboard = () => {
|
|||||||
</Text>
|
</Text>
|
||||||
<HStack spacing={4}>
|
<HStack spacing={4}>
|
||||||
{networkStatus === 'loading' && (
|
{networkStatus === 'loading' && (
|
||||||
<Badge
|
<Badge variant={'surface'} p={2} borderRadius={'lg'} colorPalette={'blue'}>
|
||||||
colorScheme={'gray'}
|
|
||||||
variant={'surface'}
|
|
||||||
p={2}
|
|
||||||
borderRadius={'lg'}
|
|
||||||
colorPalette={'blue'}
|
|
||||||
>
|
|
||||||
<Spinner size="sm" mr={2} /> {'检测网络中...'}
|
<Spinner size="sm" mr={2} /> {'检测网络中...'}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
{networkStatus === 'ok' && (
|
{networkStatus === 'ok' && (
|
||||||
<Badge
|
<Badge variant={'surface'} p={2} borderRadius={'lg'} colorPalette={'green'}>
|
||||||
colorScheme={'green'}
|
|
||||||
variant={'surface'}
|
|
||||||
p={2}
|
|
||||||
borderRadius={'lg'}
|
|
||||||
colorPalette={'green'}
|
|
||||||
>
|
|
||||||
{'网络连接正常'}
|
{'网络连接正常'}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
{networkStatus === 'fail' && (
|
{networkStatus === 'fail' && (
|
||||||
<Badge colorPalette={'red'} variant={'surface'} p={2} borderRadius={'lg'}>
|
<Badge color={'red'} variant={'surface'} p={2} borderRadius={'lg'}>
|
||||||
{'无法连接后端'}
|
{'无法连接后端'}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
|
135
src/frontend/src/pages/DevicesPage.jsx
Normal file
135
src/frontend/src/pages/DevicesPage.jsx
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
VStack,
|
||||||
|
Heading,
|
||||||
|
Text,
|
||||||
|
SimpleGrid,
|
||||||
|
Input,
|
||||||
|
Button,
|
||||||
|
HStack,
|
||||||
|
Image,
|
||||||
|
Collapsible,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
import DocumentTitle from '@/components/system/pages/DocumentTitle';
|
||||||
|
import PageContainer from '@/components/system/PageContainer';
|
||||||
|
import DashboardBackground from '@/components/system/pages/DashboardBackground';
|
||||||
|
import FadeInWrapper from '@/components/system/layout/FadeInWrapper';
|
||||||
|
import ConfigTool from '@/libs/config/ConfigTool';
|
||||||
|
import Common from '@/libs/common';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 交换机管理
|
||||||
|
* @returns {JSX.Element}
|
||||||
|
* @constructor
|
||||||
|
*/
|
||||||
|
const DevicesPage = () => {
|
||||||
|
const [devices, setDevices] = useState([]);
|
||||||
|
const [editingIndex, setEditingIndex] = useState(null);
|
||||||
|
const [editingName, setEditingName] = useState('');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const config = ConfigTool.load();
|
||||||
|
setDevices(config.devices || []);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleEdit = (idx) => {
|
||||||
|
setEditingIndex(idx);
|
||||||
|
setEditingName(devices[idx].name);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = (idx) => {
|
||||||
|
const updated = [...devices];
|
||||||
|
updated[idx].name = editingName;
|
||||||
|
setDevices(updated);
|
||||||
|
ConfigTool.save({ ...ConfigTool.load(), devices: updated });
|
||||||
|
Common.sleep(500);
|
||||||
|
setEditingIndex(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DocumentTitle title={'交换机设备'}>
|
||||||
|
<DashboardBackground />
|
||||||
|
<PageContainer>
|
||||||
|
<FadeInWrapper delay={0.3} yOffset={-5}>
|
||||||
|
<VStack gap={4} align={'stretch'}>
|
||||||
|
<Heading fontSize={'2xl'} color={'teal.300'}>
|
||||||
|
{'交换机设备管理'}
|
||||||
|
</Heading>
|
||||||
|
|
||||||
|
<SimpleGrid columns={{ base: 1, md: 3 }} gap={3}>
|
||||||
|
{devices.map((device, idx) => (
|
||||||
|
<Box
|
||||||
|
key={device.ip}
|
||||||
|
p={4}
|
||||||
|
bg={'whiteAlpha.100'}
|
||||||
|
borderRadius={'xl'}
|
||||||
|
border={'1px solid'}
|
||||||
|
borderColor={'whiteAlpha.300'}
|
||||||
|
transition={'all 0.3s ease'}
|
||||||
|
_hover={{ transform: 'translateY(-8px)', bg: 'whiteAlpha.200' }}
|
||||||
|
>
|
||||||
|
<Image src={'/assets/switch.png'} alt={'Switch'} borderRadius={'md'} mb={3} />
|
||||||
|
|
||||||
|
<Collapsible.Root open={editingIndex === idx}>
|
||||||
|
<Collapsible.Trigger asChild>
|
||||||
|
<Box mb={3} cursor={'pointer'}>
|
||||||
|
{editingIndex === idx ? null : (
|
||||||
|
<HStack justify={'space-between'}>
|
||||||
|
<Text fontSize={'lg'} fontWeight={'bold'}>
|
||||||
|
{device.name}
|
||||||
|
</Text>
|
||||||
|
<Button
|
||||||
|
size={'xs'}
|
||||||
|
colorPalette={'teal'}
|
||||||
|
onClick={() => handleEdit(idx)}
|
||||||
|
>
|
||||||
|
{'重命名'}
|
||||||
|
</Button>
|
||||||
|
</HStack>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Collapsible.Trigger>
|
||||||
|
|
||||||
|
<Collapsible.Content>
|
||||||
|
<HStack mb={3}>
|
||||||
|
<Input
|
||||||
|
value={editingName}
|
||||||
|
onChange={(e) => setEditingName(e.target.value)}
|
||||||
|
size={'sm'}
|
||||||
|
bg={'whiteAlpha.300'}
|
||||||
|
/>
|
||||||
|
<Button size={'sm'} colorPalette={'teal'} onClick={() => handleSave(idx)}>
|
||||||
|
{'保存'}
|
||||||
|
</Button>
|
||||||
|
</HStack>
|
||||||
|
</Collapsible.Content>
|
||||||
|
</Collapsible.Root>
|
||||||
|
|
||||||
|
<Text fontSize={'sm'}>
|
||||||
|
{'IP: '}
|
||||||
|
{device.ip}
|
||||||
|
</Text>
|
||||||
|
<Text fontSize={'sm'}>
|
||||||
|
{'MAC: '}
|
||||||
|
{device.mac}
|
||||||
|
</Text>
|
||||||
|
<Text fontSize={'sm'}>
|
||||||
|
{'端口: '}
|
||||||
|
{device.ports.join(', ')}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
</SimpleGrid>
|
||||||
|
|
||||||
|
{devices.length === 0 && (
|
||||||
|
<Text color={'gray.400'}>{'暂无扫描记录,请先进行网络扫描'}</Text>
|
||||||
|
)}
|
||||||
|
</VStack>
|
||||||
|
</FadeInWrapper>
|
||||||
|
</PageContainer>
|
||||||
|
</DocumentTitle>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DevicesPage;
|
@ -16,7 +16,9 @@ import PageContainer from '@/components/system/PageContainer';
|
|||||||
import DashboardBackground from '@/components/system/pages/DashboardBackground';
|
import DashboardBackground from '@/components/system/pages/DashboardBackground';
|
||||||
import FadeInWrapper from '@/components/system/layout/FadeInWrapper';
|
import FadeInWrapper from '@/components/system/layout/FadeInWrapper';
|
||||||
import { api } from '@/services/api/api';
|
import { api } from '@/services/api/api';
|
||||||
import { useNotification } from '@/libs/system/Notification';
|
import ConfigTool from '@/libs/config/ConfigTool';
|
||||||
|
import Common from '@/libs/common';
|
||||||
|
import Notification from '@/libs/system/Notification';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 网络扫描页面
|
* 网络扫描页面
|
||||||
@ -28,7 +30,9 @@ const ScanPage = () => {
|
|||||||
const [devices, setDevices] = useState([]);
|
const [devices, setDevices] = useState([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [localIp, setLocalIp] = useState('');
|
const [localIp, setLocalIp] = useState('');
|
||||||
const notify = useNotification();
|
|
||||||
|
const config = ConfigTool.load();
|
||||||
|
const testMode = config.testMode;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchLocalInfo = async () => {
|
const fetchLocalInfo = async () => {
|
||||||
@ -39,15 +43,51 @@ const ScanPage = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
fetchLocalInfo();
|
fetchLocalInfo();
|
||||||
}, [subnet, notify]);
|
}, [subnet, Notification]);
|
||||||
|
|
||||||
|
const getTestDevices = () => {
|
||||||
|
return [
|
||||||
|
{ ip: '192.168.1.1', mac: '00:1A:2B:3C:4D:5E', ports: [22, 23, 161] },
|
||||||
|
{ ip: '192.168.1.2', mac: '00:1D:7D:AA:1B:01', ports: [22, 23, 161] },
|
||||||
|
{ ip: '192.168.1.3', mac: '00:0C:29:5A:3B:11', ports: [22, 23, 161, 443] },
|
||||||
|
{ ip: '192.168.1.4', mac: '00:23:CD:FF:10:02', ports: [22, 23] },
|
||||||
|
{ ip: '192.168.1.5', mac: '00:04:4B:AA:BB:CC', ports: [22, 23, 80, 443] },
|
||||||
|
{ ip: '192.168.1.6', mac: '00:11:22:33:44:55', ports: [22, 23, 161] },
|
||||||
|
{ ip: '192.168.1.7', mac: '00:1F:45:67:89:AB', ports: [23] },
|
||||||
|
{ ip: '192.168.1.8', mac: '00:50:56:11:22:33', ports: [22, 443, 8443] },
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
const handleScan = async () => {
|
const handleScan = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
|
let scanDevices;
|
||||||
|
if (testMode) {
|
||||||
|
await Common.sleep(2000);
|
||||||
|
scanDevices = getTestDevices();
|
||||||
|
} else {
|
||||||
const res = await api.scan(subnet);
|
const res = await api.scan(subnet);
|
||||||
setDevices(res.devices || []);
|
scanDevices = res.devices || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
scanDevices = scanDevices.map((d, idx) => ({
|
||||||
|
...d,
|
||||||
|
name: `交换机 ${idx + 1}`,
|
||||||
|
}));
|
||||||
|
setDevices(scanDevices);
|
||||||
|
const updatedStats = {
|
||||||
|
totalDevices: scanDevices.length,
|
||||||
|
onlineDevices: scanDevices.length,
|
||||||
|
lastScan: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
ConfigTool.save({
|
||||||
|
...config,
|
||||||
|
stats: updatedStats,
|
||||||
|
devices: scanDevices,
|
||||||
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
notify.error({ title: '扫描网络失败' });
|
Notification.error({ title: '扫描网络失败' });
|
||||||
}
|
}
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
};
|
};
|
||||||
@ -55,10 +95,17 @@ const ScanPage = () => {
|
|||||||
const handleLastScan = async () => {
|
const handleLastScan = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
|
let scanDevices;
|
||||||
|
if (testMode) {
|
||||||
|
await Common.sleep(2000);
|
||||||
|
scanDevices = getTestDevices();
|
||||||
|
} else {
|
||||||
const res = await api.listDevices();
|
const res = await api.listDevices();
|
||||||
setDevices(res.devices || []);
|
scanDevices = res.devices || [];
|
||||||
|
}
|
||||||
|
setDevices(scanDevices);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
notify.error({ title: '获取上次扫描记录失败' });
|
Notification.error({ title: '获取上次扫描记录失败' });
|
||||||
}
|
}
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
};
|
};
|
||||||
@ -122,17 +169,42 @@ const ScanPage = () => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{!loading && devices.length > 0 && (
|
{!loading && devices.length > 0 && (
|
||||||
<Table.Root variant={'outline'} striped={'true'} size={'md'} mt={4}>
|
<FadeInWrapper delay={0.2} yOffset={-5}>
|
||||||
<Table.Header>
|
<Table.Root
|
||||||
|
variant={'outline'}
|
||||||
|
colorPalette={'teal'}
|
||||||
|
size={'md'}
|
||||||
|
striped={'false'}
|
||||||
|
showColumnBorder={'true'}
|
||||||
|
interactive={'true'}
|
||||||
|
stickyHeader={'true'}
|
||||||
|
mt={4}
|
||||||
|
borderRadius={'lg'}
|
||||||
|
overflow={'hidden'}
|
||||||
|
shadow={'lg'}
|
||||||
|
>
|
||||||
|
<Table.Header bg={'transparent'}>
|
||||||
<Table.Row>
|
<Table.Row>
|
||||||
<Table.ColumnHeader>{'IP 地址'}</Table.ColumnHeader>
|
<Table.ColumnHeader bg={'whiteAlpha.100'} backdropFilter={'blur(4px)'}>
|
||||||
<Table.ColumnHeader>{'MAC 地址'}</Table.ColumnHeader>
|
{'IP 地址'}
|
||||||
<Table.ColumnHeader>{'开放端口'}</Table.ColumnHeader>
|
</Table.ColumnHeader>
|
||||||
|
<Table.ColumnHeader bg={'whiteAlpha.100'} backdropFilter={'blur(4px)'}>
|
||||||
|
{'MAC 地址'}
|
||||||
|
</Table.ColumnHeader>
|
||||||
|
<Table.ColumnHeader bg={'whiteAlpha.100'} backdropFilter={'blur(4px)'}>
|
||||||
|
{'开放端口'}
|
||||||
|
</Table.ColumnHeader>
|
||||||
</Table.Row>
|
</Table.Row>
|
||||||
</Table.Header>
|
</Table.Header>
|
||||||
<Table.Body>
|
<Table.Body>
|
||||||
{devices.map((d) => (
|
{devices.map((d) => (
|
||||||
<Table.Row key={d.ip}>
|
<Table.Row
|
||||||
|
key={d.ip}
|
||||||
|
_hover={{
|
||||||
|
bgGradient: 'linear(to-r, teal.50, blue.50)',
|
||||||
|
transition: 'background 0.3s ease',
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Table.Cell>{d.ip}</Table.Cell>
|
<Table.Cell>{d.ip}</Table.Cell>
|
||||||
<Table.Cell>{d.mac}</Table.Cell>
|
<Table.Cell>{d.mac}</Table.Cell>
|
||||||
<Table.Cell>{d.ports.join(', ')}</Table.Cell>
|
<Table.Cell>{d.ports.join(', ')}</Table.Cell>
|
||||||
@ -140,6 +212,7 @@ const ScanPage = () => {
|
|||||||
))}
|
))}
|
||||||
</Table.Body>
|
</Table.Body>
|
||||||
</Table.Root>
|
</Table.Root>
|
||||||
|
</FadeInWrapper>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!loading && devices.length === 0 && (
|
{!loading && devices.length === 0 && (
|
||||||
|
@ -2,38 +2,16 @@ import axios from 'axios';
|
|||||||
import ConfigTool from '@/libs/config/ConfigTool';
|
import ConfigTool from '@/libs/config/ConfigTool';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建带基础URL的axios实例
|
* 动态拼接完整URL
|
||||||
*/
|
*/
|
||||||
const apiClient = axios.create({
|
const buildUrl = (path) => {
|
||||||
baseURL: ConfigTool.load().backendUrl || '',
|
const config = ConfigTool.load();
|
||||||
timeout: 10000,
|
let baseUrl = config.backendUrl || '';
|
||||||
headers: {
|
if (baseUrl.endsWith('/')) {
|
||||||
'Content-Type': 'application/json',
|
baseUrl = baseUrl.slice(0, -1);
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// 请求拦截器
|
|
||||||
apiClient.interceptors.request.use(
|
|
||||||
(config) => {
|
|
||||||
const cfg = ConfigTool.load();
|
|
||||||
if (cfg?.authKey) {
|
|
||||||
config.headers['Authorization'] = `Bearer ${cfg.authKey}`;
|
|
||||||
}
|
}
|
||||||
return config;
|
return `${baseUrl}${path}`;
|
||||||
},
|
};
|
||||||
(error) => {
|
|
||||||
return Promise.reject(error);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// 响应拦截器
|
|
||||||
apiClient.interceptors.response.use(
|
|
||||||
(response) => response.data,
|
|
||||||
(error) => {
|
|
||||||
console.error('API请求错误:', error);
|
|
||||||
return Promise.reject(error.response?.data || error.message);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* API模块
|
* API模块
|
||||||
@ -43,27 +21,27 @@ export const api = {
|
|||||||
* 测试API连接
|
* 测试API连接
|
||||||
* @returns {Promise<axios.AxiosResponse<any>>}
|
* @returns {Promise<axios.AxiosResponse<any>>}
|
||||||
*/
|
*/
|
||||||
test: () => apiClient.get('/api/test'),
|
test: () => axios.get(buildUrl('/api/test')),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 扫描网络
|
* 扫描网络
|
||||||
* @param subnet 子网地址
|
* @param subnet 子网地址
|
||||||
* @returns {Promise<axios.AxiosResponse<any>>}
|
* @returns {Promise<axios.AxiosResponse<any>>}
|
||||||
*/
|
*/
|
||||||
scan: (subnet) => apiClient.get(`/api/scan_network`, { params: { subnet } }),
|
scan: (subnet) => axios.get(buildUrl('/api/scan_network'), { params: { subnet } }),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 列出所有设备
|
* 列出所有设备
|
||||||
* @returns {Promise<axios.AxiosResponse<any>>}
|
* @returns {Promise<axios.AxiosResponse<any>>}
|
||||||
*/
|
*/
|
||||||
listDevices: () => apiClient.get('/api/list_devices'),
|
listDevices: () => axios.get(buildUrl('/api/list_devices')),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 解析命令
|
* 解析命令
|
||||||
* @param text 文本
|
* @param text 文本
|
||||||
* @returns {Promise<axios.AxiosResponse<any>>}
|
* @returns {Promise<axios.AxiosResponse<any>>}
|
||||||
*/
|
*/
|
||||||
parseCommand: (text) => apiClient.post('/api/parse_command', { command: text }),
|
parseCommand: (text) => axios.post(buildUrl('/api/parse_command'), { command: text }),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 应用配置
|
* 应用配置
|
||||||
@ -71,7 +49,8 @@ export const api = {
|
|||||||
* @param config 配置
|
* @param config 配置
|
||||||
* @returns {Promise<axios.AxiosResponse<any>>}
|
* @returns {Promise<axios.AxiosResponse<any>>}
|
||||||
*/
|
*/
|
||||||
applyConfig: (switch_ip, config) => apiClient.post('/api/apply_config', { switch_ip, config }),
|
applyConfig: (switch_ip, config) =>
|
||||||
|
axios.post(buildUrl('/api/apply_config'), { switch_ip, config }),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 更新基础URL
|
* 更新基础URL
|
||||||
@ -81,7 +60,6 @@ export const api = {
|
|||||||
const config = ConfigTool.load();
|
const config = ConfigTool.load();
|
||||||
config.backendUrl = url;
|
config.backendUrl = url;
|
||||||
ConfigTool.save(config);
|
ConfigTool.save(config);
|
||||||
apiClient.defaults.baseURL = url;
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -97,4 +75,4 @@ export const getConfig = () => ConfigTool.load();
|
|||||||
*/
|
*/
|
||||||
export const getBaseUrl = () => ConfigTool.load().backendUrl || '';
|
export const getBaseUrl = () => ConfigTool.load().backendUrl || '';
|
||||||
|
|
||||||
export default apiClient;
|
export default api;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user