通知hook

This commit is contained in:
Jerry 2025-06-19 18:45:10 +08:00
parent c186b8f3bb
commit a528009674
6 changed files with 182 additions and 21 deletions

View File

@ -1,9 +1,11 @@
import { BrowserRouter, Routes, Route } from 'react-router-dom'; 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';
const App = () => { const App = () => {
const isProd = process.env.NODE_ENV === 'production'; const isProd = process.env.NODE_ENV === 'production';
useEffect(() => {}, []);
return ( return (
<BrowserRouter basename={isProd ? '/AI-powered-switches' : '/'}> <BrowserRouter basename={isProd ? '/AI-powered-switches' : '/'}>
<Routes> <Routes>

View File

@ -1,7 +1,7 @@
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 { Button, HStack } from '@chakra-ui/react'; import { Box, Button, HStack } 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';
@ -31,10 +31,10 @@ const GithubTransitionCard = () => {
}, 400); }, 400);
return () => clearTimeout(timer); return () => clearTimeout(timer);
}, [isDashboard]); }, [isDashboard]);
const MotionBox = motion(Box);
return ( return (
<AnimatePresence mode={'wait'}> <AnimatePresence mode={'wait'}>
<motion.div <MotionBox
key={isDashboard ? 'dashboard' : 'welcome'} key={isDashboard ? 'dashboard' : 'welcome'}
initial={{ opacity: 0, height: 'auto', width: isDashboard ? 200 : 'auto' }} initial={{ opacity: 0, height: 'auto', width: isDashboard ? 200 : 'auto' }}
animate={{ animate={{
@ -95,7 +95,7 @@ const GithubTransitionCard = () => {
)} )}
</MotionCard> </MotionCard>
</FadeInWrapper> </FadeInWrapper>
</motion.div> </MotionBox>
</AnimatePresence> </AnimatePresence>
); );
}; };

View File

@ -2,10 +2,13 @@ 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>
); );

View File

@ -0,0 +1,113 @@
import React, { createContext, useContext, useState, useCallback } from 'react';
import { Box, Alert, Button, Icon, Text } from '@chakra-ui/react';
import { AnimatePresence, motion } from 'framer-motion';
import { AiOutlineInfoCircle, AiFillWarning } from 'react-icons/ai';
const NotificationContext = createContext(null);
/**
* 通知hook
* @returns {null}
*/
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);
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
colorScheme={'whiteAlpha'}
size={'sm'}
mt={3}
onClick={item.button.onClick}
>
{item.button.label}
</Button>
)}
</Box>
</MotionBox>
))}
</AnimatePresence>
</Box>
</NotificationContext.Provider>
);
};

View File

@ -1,17 +1,13 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState, useCallback } from 'react';
import { Box, Text, VStack, HStack, SimpleGrid, Badge } from '@chakra-ui/react'; import { Box, Text, VStack, HStack, SimpleGrid, Badge, Button, Spinner } from '@chakra-ui/react';
import DocumentTitle from '@/components/system/pages/DocumentTitle'; import DocumentTitle from '@/components/system/pages/DocumentTitle';
import PageContainer from '@/components/system/PageContainer'; 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 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';
/**
* 控制台页面
* @returns {JSX.Element}
* @constructor
*/
const Dashboard = () => { const Dashboard = () => {
const [stats, setStats] = useState({ const [stats, setStats] = useState({
totalDevices: 0, totalDevices: 0,
@ -19,7 +15,33 @@ const Dashboard = () => {
lastScan: '', lastScan: '',
}); });
useEffect(() => {}, []); const [networkStatus, setNetworkStatus] = useState('idle'); // idle | loading | ok | fail
const notify = useNotification();
const checkBackend = useCallback(async () => {
setNetworkStatus('loading');
try {
const res = await fetch('/api/test');
if (res.ok) {
setNetworkStatus('ok');
notify.info({ title: '成功连接至后端服务!' });
} else {
setNetworkStatus('fail');
notify.error({ title: '后端服务响应异常!' });
}
} catch (err) {
setNetworkStatus('fail');
notify.error({ title: '无法连接到后端服务!' });
}
}, [notify]);
useEffect(() => {
const timer = setTimeout(() => {
checkBackend();
}, 3000);
return () => clearTimeout(timer);
}, [checkBackend]);
return ( return (
<DocumentTitle title={'控制台'}> <DocumentTitle title={'控制台'}>
@ -38,10 +60,10 @@ const Dashboard = () => {
_hover={{ transform: 'translateY(-4px)' }} _hover={{ transform: 'translateY(-4px)' }}
> >
<Text fontSize={'3xl'} fontWeight={'bold'} color={'teal.300'}> <Text fontSize={'3xl'} fontWeight={'bold'} color={'teal.300'}>
{`欢迎使用智能交换机管理系统`} {'欢迎使用智能交换机管理系统'}
</Text> </Text>
<Text mt={2} fontSize={'lg'} color={'gray.300'}> <Text mt={2} fontSize={'lg'} color={'gray.300'}>
{`实时监控您的网络设备状态,快速配置并掌控全局网络环境`} {'实时监控您的网络设备状态,快速配置并掌控全局网络环境'}
</Text> </Text>
</Box> </Box>
@ -67,19 +89,40 @@ const Dashboard = () => {
_hover={{ transform: 'translateY(-4px)' }} _hover={{ transform: 'translateY(-4px)' }}
> >
<Text fontSize={'xl'} fontWeight={'bold'} mb={4} color={'white'}> <Text fontSize={'xl'} fontWeight={'bold'} mb={4} color={'white'}>
{`网络健康状态`} {'网络健康状态'}
</Text> </Text>
<HStack spacing={4}> <HStack spacing={4}>
<Badge colorScheme={'green'} variant={'solid'} p={2} borderRadius={'lg'}> {networkStatus === 'idle' && (
{`网络连接正常`} <Badge colorScheme={'gray'} variant={'solid'} p={2} borderRadius={'lg'}>
{'等待检测'}
</Badge> </Badge>
)}
{networkStatus === 'loading' && (
<Badge colorScheme={'gray'} variant={'solid'} p={2} borderRadius={'lg'}>
<Spinner size="sm" mr={2} /> {'检测网络中...'}
</Badge>
)}
{networkStatus === 'ok' && (
<Badge colorScheme={'green'} variant={'solid'} p={2} borderRadius={'lg'}>
{'网络连接正常'}
</Badge>
)}
{networkStatus === 'fail' && (
<Badge colorScheme={'red'} variant={'solid'} p={2} borderRadius={'lg'}>
{'无法连接后端'}
</Badge>
)}
<Badge colorScheme={'blue'} variant={'solid'} p={2} borderRadius={'lg'}> <Badge colorScheme={'blue'} variant={'solid'} p={2} borderRadius={'lg'}>
{`交换机正常运行`} {'交换机正常运行'}
</Badge> </Badge>
<Badge colorScheme={'yellow'} variant={'solid'} p={2} borderRadius={'lg'}> <Badge colorScheme={'yellow'} variant={'solid'} p={2} borderRadius={'lg'}>
{`流量监控未启动`} {'流量监控未启动'}
</Badge> </Badge>
</HStack> </HStack>
<Button mt={4} onClick={checkBackend} colorScheme={'teal'}>
{'重新检测'}
</Button>
</Box> </Box>
</FadeInWrapper> </FadeInWrapper>