Compare commits

..

No commits in common. "223d43f0d9c7cbb7209d2b1df3f320473ac801ea" and "3070c676f899ab434733848c46aeaaf80b068669" have entirely different histories.

8 changed files with 69 additions and 344 deletions

View File

@ -7,7 +7,6 @@ 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'; import ConfigTool from '@/libs/config/ConfigTool';
import Notification from '@/libs/system/Notification';
const navItems = [ const navItems = [
{ label: '面板', path: '/dashboard' }, { label: '面板', path: '/dashboard' },
@ -47,10 +46,6 @@ const GithubTransitionCard = () => {
setTestMode(checked); setTestMode(checked);
const config = ConfigTool.load(); const config = ConfigTool.load();
ConfigTool.save({ ...config, testMode: checked }); ConfigTool.save({ ...config, testMode: checked });
let mode = checked ? '调试模式' : '常规模式';
Notification.success({
title: `成功切换至${mode}!`,
});
}; };
const MotionBox = motion(Box); const MotionBox = motion(Box);
@ -114,9 +109,9 @@ const GithubTransitionCard = () => {
{item.label} {item.label}
</Button> </Button>
))} ))}
<HStack spacing={4} ml={'right'}> <HStack spacing={4} ml={'auto'}>
<Text fontSize={'sm'} color={'white'}> <Text fontSize={'sm'} color={'white'}>
{'试模式'} {'试模式'}
</Text> </Text>
<Switch.Root <Switch.Root
size={'sm'} size={'sm'}

View File

@ -4,7 +4,6 @@ import {
AiFillWarning, AiFillWarning,
AiFillCheckCircle, AiFillCheckCircle,
AiFillExclamationCircle, AiFillExclamationCircle,
AiOutlineLoading,
} from 'react-icons/ai'; } from 'react-icons/ai';
const surfaceStyle = { const surfaceStyle = {
@ -17,13 +16,7 @@ const surfaceStyle = {
/** /**
* 通用通知组件 * 通用通知组件
* @type {{ * @type {{info({title: *, description: *, button: *}): void, success({title: *, description: *, button: *}): void, warn({title: *, description: *, button: *}): void, error({title: *, description: *, button: *}): void}}
* info({title: *, description: *, button: *}): void,
* success({title: *, description: *, button: *}): void,
* warn({title: *, description: *, button: *}): void,
* error({title: *, description: *, button: *}): void,
* promise({promise: *, loading: *, success: *, error: *}): void
* }}
*/ */
const Notification = { const Notification = {
/** /**
@ -64,38 +57,6 @@ const Notification = {
}); });
}, },
/**
* 异步操作通知
* @param {Object} params 参数对象
* @param {Promise} params.promise 异步操作Promise
* @param {Object} params.loading 加载中状态配置
* @param {Object} params.success 成功状态配置
* @param {Object} params.error 错误状态配置
* @returns {{id: string | undefined, unwrap: () => Promise<unknown>}}
*/
async promise({ promise, loading, success, error }) {
return toaster.promise(promise, {
loading: {
title: loading.title,
description: loading.description,
icon: <AiOutlineLoading className={'animate-spin'} size={24} />,
style: surfaceStyle,
},
success: {
title: success.title,
description: success.description,
icon: <AiFillCheckCircle size={24} />,
style: surfaceStyle,
},
error: {
title: error.title,
description: error.description,
icon: <AiFillWarning size={24} />,
style: surfaceStyle,
},
});
},
/** /**
* 警告 * 警告
* @param title 标题 * @param title 标题

View File

@ -1,17 +1,5 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { import { Box, VStack, Heading, Textarea, Button, Select, Text } from '@chakra-ui/react';
Box,
Button,
createListCollection,
Field,
Heading,
HStack,
Portal,
Select,
Text,
Textarea,
VStack,
} 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';
@ -19,184 +7,44 @@ import FadeInWrapper from '@/components/system/layout/FadeInWrapper';
import ConfigTool from '@/libs/config/ConfigTool'; import ConfigTool from '@/libs/config/ConfigTool';
import { api } from '@/services/api/api'; import { api } from '@/services/api/api';
import Notification from '@/libs/system/Notification'; import Notification from '@/libs/system/Notification';
import Common from '@/libs/common';
const testMode = ConfigTool.load().testMode;
const ConfigPage = () => { const ConfigPage = () => {
const [devices, setDevices] = useState([]); const [devices, setDevices] = useState([]);
const [selectedDevice, setSelectedDevice] = useState(''); const [selectedDevice, setSelectedDevice] = useState('');
const [inputText, setInputText] = useState(''); const [inputText, setInputText] = useState('');
const [parsedConfig, setParsedConfig] = useState(''); const [parsedConfig, setParsedConfig] = useState('');
const [editableConfig, setEditableConfig] = useState(''); const [loading, setLoading] = useState(false);
const [applying, setApplying] = useState(false);
const [hasParsed, setHasParsed] = useState(false);
const deviceCollection = createListCollection({
items: devices.map((device) => ({
label: `${device.name} (${device.ip})`,
value: device.ip,
})),
});
useEffect(() => { useEffect(() => {
const config = ConfigTool.load(); const config = ConfigTool.load();
setDevices(config.devices || []); setDevices(config.devices || []);
}, []); }, []);
const generateRealisticConfig = (command, devices = []) => {
const timestamp = new Date().toLocaleString();
let config = `! 配置生成于 ${timestamp}\n`;
const cmd = command.toLowerCase();
// VLAN
if (cmd.includes('vlan')) {
const vlanIdMatch = command.match(/vlan\s*(\d+)/i);
const vlanId = vlanIdMatch?.[1] || '10';
const isMgmt = cmd.includes('管理') || cmd.includes('mgmt');
config +=
`vlan ${vlanId}\n` +
` name ${isMgmt ? 'MGMT' : 'USER'}_VLAN\n` +
` exit\n` +
`interface Vlan${vlanId}\n` +
` description ${isMgmt ? 'Management VLAN' : 'User VLAN'}\n` +
` ip address 192.168.${vlanId}.1 255.255.255.0\n` +
` exit\n`;
}
// SSH
if (cmd.includes('ssh') || cmd.includes('安全') || cmd.includes('登录')) {
const password = Math.random().toString(36).slice(2, 10);
config +=
`ip ssh server\n` +
`ip ssh version 2\n` +
`username admin privilege 15 secret 0 ${password}\n` +
`line vty 0 4\n` +
` transport input ssh\n` +
` login local\n` +
` exit\n`;
}
//
if (cmd.includes('端口') || cmd.includes('接口') || cmd.includes('port')) {
const portMatch = command.match(/端口\s*(\d+)/i) || command.match(/port\s*(\d+)/i);
const port = portMatch?.[1] || '1';
const isTrunk = cmd.includes('trunk');
const isAccess = cmd.includes('access') || !isTrunk;
const desc = cmd.includes('上联') || cmd.includes('uplink') ? 'Uplink_Port' : 'Access_Port';
const vlanId = '10';
config +=
`interface GigabitEthernet0/${port}\n` +
` description ${desc}\n` +
` switchport mode ${isTrunk ? 'trunk' : 'access'}\n` +
` ${isTrunk ? 'switchport trunk allowed vlan all' : `switchport access vlan ${vlanId}`}\n` +
` no shutdown\n` +
` exit\n`;
}
// ACL
if (cmd.includes('acl') || cmd.includes('访问控制') || cmd.includes('防火墙')) {
let targetIP = '192.168.10.10';
if (devices.length > 0) {
const randomDevice = devices[Math.floor(Math.random() * devices.length)];
targetIP = randomDevice.ip;
}
config +=
`ip access-list extended PROTECT_SERVERS\n` +
` permit tcp any host ${targetIP} eq 22\n` +
` permit tcp any host ${targetIP} eq 80\n` +
` deny ip any any\n` +
` exit\n` +
`interface Vlan10\n` +
` ip access-group PROTECT_SERVERS in\n` +
` exit\n`;
}
if (config.trim() === `! 配置生成于 ${timestamp}`) {
config += '! 当前命令未识别到任何可配置项目\n';
}
return { config };
};
const handleParse = async () => { const handleParse = async () => {
if (!selectedDevice || !inputText.trim()) { if (!selectedDevice || !inputText) {
Notification.error({ Notification.error({ title: '请选择设备并输入配置指令' });
title: '操作失败',
description: '请选择设备并输入配置指令',
});
return; return;
} }
setLoading(true);
try { try {
const performParse = async () => { const res = await api.parseCommand(inputText);
if (testMode) { setParsedConfig(res.config);
await Common.sleep(800 + Math.random() * 700); } catch (e) {
return generateRealisticConfig(inputText); Notification.error({ title: '配置解析失败' });
}
return await api.parseCommand(inputText);
};
const resultWrapper = await Notification.promise({
promise: performParse(),
loading: {
title: '正在解析配置',
description: '正在分析您的指令...',
},
success: {
title: '解析完成',
description: '已生成交换机配置',
},
error: {
title: '解析失败',
description: '请检查指令格式或网络连接',
},
});
const result = await resultWrapper.unwrap();
if (result?.config) {
setParsedConfig(result.config);
setEditableConfig(result.config);
setHasParsed(true);
}
} catch (error) {
console.error('配置解析异常:', error);
Notification.error({
title: '配置解析异常',
description: error.message,
});
} }
setLoading(false);
}; };
const handleApply = async () => { const handleApply = async () => {
if (!editableConfig.trim()) { if (!parsedConfig) return;
Notification.warn({ setLoading(true);
title: '配置为空',
description: '请先解析或编辑有效配置',
});
return;
}
setApplying(true);
try { try {
const applyOperation = testMode await api.applyConfig(selectedDevice, parsedConfig);
? Common.sleep(1000).then(() => ({ success: true })) Notification.success({ title: '配置已成功应用!' });
: await api.applyConfig(selectedDevice, editableConfig); } catch (e) {
Notification.error({ title: '配置应用失败' });
await Notification.promise({
promise: applyOperation,
loading: {
title: '配置应用中',
description: '正在推送配置到设备...',
},
success: {
title: '应用成功',
description: '配置已成功生效',
},
error: {
title: '应用失败',
description: '请检查设备连接或配置内容',
},
});
} finally {
setApplying(false);
} }
setLoading(false);
}; };
return ( return (
@ -204,73 +52,42 @@ const ConfigPage = () => {
<DashboardBackground /> <DashboardBackground />
<PageContainer> <PageContainer>
<FadeInWrapper delay={0.3} yOffset={-5}> <FadeInWrapper delay={0.3} yOffset={-5}>
<VStack spacing={6} align={'stretch'}> <VStack spacing={8} align={'stretch'}>
<Heading fontSize={'xl'} color={'teal.300'}> <Heading fontSize={'2xl'} color={'teal.300'}>
交换机配置中心 {'交换机配置中心'}
</Heading> </Heading>
<Field.Root> <Select
<Field.Label fontWeight={'bold'} mb={1} fontSize="sm">
选择交换机设备
</Field.Label>
<Select.Root
collection={deviceCollection}
value={selectedDevice ? [selectedDevice] : []}
onValueChange={({ value }) => setSelectedDevice(value[0] ?? '')}
placeholder={'请选择交换机设备'} placeholder={'请选择交换机设备'}
size={'sm'} value={selectedDevice}
colorPalette={'teal'} onChange={(e) => setSelectedDevice(e.target.value)}
bg={'whiteAlpha.200'}
> >
<Select.HiddenSelect /> {devices.map((device) => (
<Select.Control> <option key={device.ip} value={device.ip}>
<Select.Trigger> {device.name} ({device.ip})
<Select.ValueText /> </option>
</Select.Trigger>
<Select.IndicatorGroup>
<Select.Indicator />
<Select.ClearTrigger />
</Select.IndicatorGroup>
</Select.Control>
<Portal>
<Select.Positioner>
<Select.Content>
{deviceCollection.items.map((item) => (
<Select.Item key={item.value} item={item}>
{item.label}
</Select.Item>
))} ))}
</Select.Content> </Select>
</Select.Positioner>
</Portal>
</Select.Root>
</Field.Root>
<Field.Root>
<Field.Label fontWeight={'bold'} mb={1} fontSize="sm">
配置指令输入
</Field.Label>
<Textarea <Textarea
rows={4} rows={6}
placeholder={'例子创建VLAN 10并配置IP 192.168.10.1/24并在端口1启用SSH访问"'} placeholder={'输入自然语言配置指令...'}
value={inputText} value={inputText}
onChange={(e) => setInputText(e.target.value)} onChange={(e) => setInputText(e.target.value)}
bg={'whiteAlpha.200'} bg={'whiteAlpha.200'}
size={'sm'}
/> />
</Field.Root>
<Button <Button
colorScheme={'teal'} colorPalette={'teal'}
variant={'solid'} variant={'solid'}
size={'sm'} isLoading={loading}
onClick={handleParse} onClick={handleParse}
isDisabled={!selectedDevice || !inputText.trim()}
> >
解析配置 {'解析配置'}
</Button> </Button>
{hasParsed && ( {parsedConfig && (
<FadeInWrapper>
<Box <Box
p={4} p={4}
bg={'whiteAlpha.100'} bg={'whiteAlpha.100'}
@ -278,44 +95,14 @@ const ConfigPage = () => {
border={'1px solid'} border={'1px solid'}
borderColor={'whiteAlpha.300'} borderColor={'whiteAlpha.300'}
> >
<Text fontWeight={'bold'} mb={2} fontSize="sm"> <Text fontWeight={'bold'} mb={2}>
生成配置: {'已生成配置:'}
</Text> </Text>
<Textarea <Textarea value={parsedConfig} rows={8} readOnly />
value={editableConfig} <Button mt={4} colorPalette={'teal'} onClick={handleApply}>
rows={12} {'应用到交换机'}
onChange={(e) => setEditableConfig(e.target.value)}
fontFamily={'monospace'}
size={'sm'}
bg={'blackAlpha.200'}
/>
<HStack mt={4} spacing={3} justify={'flex-end'}>
<Button
variant={'outline'}
colorScheme={'gray'}
size={'sm'}
onClick={() => {
setEditableConfig(parsedConfig);
Notification.success({
title: '成功重置配置!',
description: '现在您可以重新审查生成的配置',
});
}}
>
重置为原始配置
</Button> </Button>
<Button
colorScheme={'teal'}
size={'sm'}
onClick={handleApply}
isLoading={applying}
isDisabled={!editableConfig.trim()}
>
应用到交换机
</Button>
</HStack>
</Box> </Box>
</FadeInWrapper>
)} )}
</VStack> </VStack>
</FadeInWrapper> </FadeInWrapper>

View File

@ -31,21 +31,18 @@ const Dashboard = () => {
if (res) { if (res) {
setNetworkStatus('ok'); setNetworkStatus('ok');
console.log(JSON.stringify(res)); console.log(JSON.stringify(res));
Notification.info({ title: '成功连接至后端服务!', description: res.message }); Notification.info({ title: '成功连接至后端服务!' });
} else { } else {
setNetworkStatus('fail'); setNetworkStatus('fail');
Notification.error({ title: '后端服务响应异常!', description: JSON.stringify(res) }); Notification.error({ title: '后端服务响应异常!' });
} }
} catch (err) { } catch (err) {
setNetworkStatus('fail'); setNetworkStatus('fail');
Notification.error({ title: '无法连接到后端服务!', description: err.message }); Notification.error({ title: '无法连接到后端服务!' });
} }
}, [Notification]); }, [Notification]);
useEffect(() => { useEffect(() => {
Notification.info({
title: '正在尝试连接后端服务',
});
const timer = setTimeout(() => { const timer = setTimeout(() => {
checkBackend(); checkBackend();
}, 3000); }, 3000);

View File

@ -17,8 +17,6 @@ import DashboardBackground from '@/components/system/pages/DashboardBackground';
import FadeInWrapper from '@/components/system/layout/FadeInWrapper'; import FadeInWrapper from '@/components/system/layout/FadeInWrapper';
import ConfigTool from '@/libs/config/ConfigTool'; import ConfigTool from '@/libs/config/ConfigTool';
import Common from '@/libs/common'; import Common from '@/libs/common';
import switchIcon from '@/resources/icon/pages/devices/switch.png';
import Notification from '@/libs/system/Notification';
/** /**
* 交换机管理 * 交换机管理
@ -43,12 +41,9 @@ const DevicesPage = () => {
const handleSave = (idx) => { const handleSave = (idx) => {
const updated = [...devices]; const updated = [...devices];
updated[idx].name = editingName; updated[idx].name = editingName;
Common.sleep(200);
setDevices(updated); setDevices(updated);
ConfigTool.save({ ...ConfigTool.load(), devices: updated }); ConfigTool.save({ ...ConfigTool.load(), devices: updated });
Notification.success({ Common.sleep(500);
title: '设备重命名成功!',
});
setEditingIndex(null); setEditingIndex(null);
}; };
@ -74,7 +69,7 @@ const DevicesPage = () => {
transition={'all 0.3s ease'} transition={'all 0.3s ease'}
_hover={{ transform: 'translateY(-8px)', bg: 'whiteAlpha.200' }} _hover={{ transform: 'translateY(-8px)', bg: 'whiteAlpha.200' }}
> >
<Image src={switchIcon} alt={'Switch'} borderRadius={'md'} mb={3} /> <Image src={'/assets/switch.png'} alt={'Switch'} borderRadius={'md'} mb={3} />
<Collapsible.Root open={editingIndex === idx}> <Collapsible.Root open={editingIndex === idx}>
<Collapsible.Trigger asChild> <Collapsible.Trigger asChild>

View File

@ -36,9 +36,9 @@ const ScanPage = () => {
useEffect(() => { useEffect(() => {
const fetchLocalInfo = async () => { const fetchLocalInfo = async () => {
setLocalIp('192.168.1.100'); setLocalIp('172.17.99.208');
if (!subnet) { if (!subnet) {
const ipParts = '192.168.1.0'.split('.'); const ipParts = '172.17.99.208'.split('.');
setSubnet(`${ipParts[0]}.${ipParts[1]}.${ipParts[2]}.0/24`); setSubnet(`${ipParts[0]}.${ipParts[1]}.${ipParts[2]}.0/24`);
} }
}; };
@ -65,15 +65,9 @@ const ScanPage = () => {
if (testMode) { if (testMode) {
await Common.sleep(2000); await Common.sleep(2000);
scanDevices = getTestDevices(); scanDevices = getTestDevices();
Notification.success({
title: '扫描子网设备成功!',
});
} else { } else {
const res = await api.scan(subnet); const res = await api.scan(subnet);
scanDevices = res.devices || []; scanDevices = res.devices || [];
Notification.success({
title: '扫描子网设备成功!',
});
} }
scanDevices = scanDevices.map((d, idx) => ({ scanDevices = scanDevices.map((d, idx) => ({
@ -103,11 +97,8 @@ const ScanPage = () => {
try { try {
let scanDevices; let scanDevices;
if (testMode) { if (testMode) {
await Common.sleep(500); await Common.sleep(2000);
scanDevices = getTestDevices(); scanDevices = getTestDevices();
Notification.success({
title: '获取上一次扫描记录成功!',
});
} else { } else {
const res = await api.listDevices(); const res = await api.listDevices();
scanDevices = res.devices || []; scanDevices = res.devices || [];

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 24 24"><path fill="#41d7e1" d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10s10-4.5 10-10S17.5 2 12 2m0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8s8 3.59 8 8s-3.59 8-8 8M6.5 9L10 5.5L13.5 9H11v4H9V9zm11 6L14 18.5L10.5 15H13v-4h2v4z"/></svg>

Before

Width:  |  Height:  |  Size: 298 B