diff --git a/src/frontend/src/components/system/layout/github/GithubTransitionCard.jsx b/src/frontend/src/components/system/layout/github/GithubTransitionCard.jsx index c2f5e31..e5d3bc7 100644 --- a/src/frontend/src/components/system/layout/github/GithubTransitionCard.jsx +++ b/src/frontend/src/components/system/layout/github/GithubTransitionCard.jsx @@ -7,6 +7,7 @@ import githubIcon from '@/resources/icon/pages/weclome/github.svg'; import FadeInWrapper from '@/components/system/layout/FadeInWrapper'; import MotionCard from '@/components/ui/MotionCard'; import ConfigTool from '@/libs/config/ConfigTool'; +import Notification from '@/libs/system/Notification'; const navItems = [ { label: '面板', path: '/dashboard' }, @@ -46,6 +47,10 @@ const GithubTransitionCard = () => { setTestMode(checked); const config = ConfigTool.load(); ConfigTool.save({ ...config, testMode: checked }); + let mode = checked ? '调试模式' : '常规模式'; + Notification.success({ + title: `成功切换至${mode}!`, + }); }; const MotionBox = motion(Box); @@ -109,9 +114,9 @@ const GithubTransitionCard = () => { {item.label} ))} - + - {'测试模式'} + {'调试模式'} Promise}} + */ + async promise({ promise, loading, success, error }) { + return toaster.promise(promise, { + loading: { + title: loading.title, + description: loading.description, + icon: , + style: surfaceStyle, + }, + success: { + title: success.title, + description: success.description, + icon: , + style: surfaceStyle, + }, + error: { + title: error.title, + description: error.description, + icon: , + style: surfaceStyle, + }, + }); + }, + /** * 警告 * @param title 标题 diff --git a/src/frontend/src/pages/ConfigPage.jsx b/src/frontend/src/pages/ConfigPage.jsx index d0704bc..c4c74a8 100644 --- a/src/frontend/src/pages/ConfigPage.jsx +++ b/src/frontend/src/pages/ConfigPage.jsx @@ -1,5 +1,17 @@ import React, { useEffect, useState } from 'react'; -import { Box, VStack, Heading, Textarea, Button, Select, Text } from '@chakra-ui/react'; +import { + Box, + Button, + createListCollection, + Field, + Heading, + HStack, + Portal, + Select, + Text, + Textarea, + VStack, +} from '@chakra-ui/react'; import DocumentTitle from '@/components/system/pages/DocumentTitle'; import PageContainer from '@/components/system/PageContainer'; import DashboardBackground from '@/components/system/pages/DashboardBackground'; @@ -7,44 +19,167 @@ 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'; +import Common from '@/libs/common'; + +const testMode = ConfigTool.load().testMode; const ConfigPage = () => { const [devices, setDevices] = useState([]); const [selectedDevice, setSelectedDevice] = useState(''); const [inputText, setInputText] = useState(''); const [parsedConfig, setParsedConfig] = useState(''); - const [loading, setLoading] = useState(false); + const [editableConfig, setEditableConfig] = useState(''); + 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(() => { const config = ConfigTool.load(); setDevices(config.devices || []); }, []); + const generateRealisticConfig = (command) => { + const timestamp = new Date().toLocaleString(); + let config = `! 配置生成于 ${timestamp}\n`; + + if (command.includes('VLAN')) { + const vlanId = command.match(/VLAN\s*(\d+)/)?.[1] || '10'; + config += + `vlan ${vlanId}\n` + + ` name ${command.includes('管理') ? 'MGMT' : 'USER'}_VLAN\n` + + ` exit\n` + + `interface Vlan${vlanId}\n` + + ` description ${command.includes('管理') ? 'Management' : 'User'} VLAN\n` + + ` ip address 192.168.${vlanId}.1 255.255.255.0\n` + + ` exit\n`; + } + + if (command.includes('SSH') || command.includes('安全')) { + config += + `ip ssh server\n` + + `ip ssh version 2\n` + + `username admin privilege 15 secret 0 ${Math.random().toString(36).slice(2, 10)}\n` + + `line vty 0 4\n` + + ` transport input ssh\n` + + ` login local\n` + + ` exit\n`; + } + + if (command.includes('端口') || command.includes('接口')) { + const port = command.match(/端口\s*(\d+)/)?.[1] || '1'; + config += + `interface GigabitEthernet0/${port}\n` + + ` description ${command.includes('接入') ? 'Access_Port' : 'Uplink_Port'}\n` + + ` switchport mode ${command.includes('trunk') ? 'trunk' : 'access'}\n` + + ` ${command.includes('trunk') ? 'switchport trunk allowed vlan all' : 'switchport access vlan 10'}\n` + + ` no shutdown\n` + + ` exit\n`; + } + + if (command.includes('ACL') || command.includes('访问控制')) { + config += + `ip access-list extended PROTECT_SERVERS\n` + + ` permit tcp any host 192.168.10.10 eq 22\n` + + ` permit tcp any host 192.168.10.10 eq 80\n` + + ` deny ip any any\n` + + ` exit\n` + + `interface Vlan10\n` + + ` ip access-group PROTECT_SERVERS in\n` + + ` exit\n`; + } + + return { config }; + }; + const handleParse = async () => { - if (!selectedDevice || !inputText) { - Notification.error({ title: '请选择设备并输入配置指令' }); + if (!selectedDevice || !inputText.trim()) { + Notification.error({ + title: '操作失败', + description: '请选择设备并输入配置指令', + }); return; } - setLoading(true); + try { - const res = await api.parseCommand(inputText); - setParsedConfig(res.config); - } catch (e) { - Notification.error({ title: '配置解析失败' }); + const performParse = async () => { + if (testMode) { + await Common.sleep(800 + Math.random() * 700); + return generateRealisticConfig(inputText); + } + 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 () => { - if (!parsedConfig) return; - setLoading(true); - try { - await api.applyConfig(selectedDevice, parsedConfig); - Notification.success({ title: '配置已成功应用!' }); - } catch (e) { - Notification.error({ title: '配置应用失败' }); + if (!editableConfig.trim()) { + Notification.warn({ + title: '配置为空', + description: '请先解析或编辑有效配置', + }); + return; + } + + setApplying(true); + try { + const applyOperation = testMode + ? Common.sleep(1000).then(() => ({ success: true })) + : await api.applyConfig(selectedDevice, editableConfig); + + await Notification.promise({ + promise: applyOperation, + loading: { + title: '配置应用中', + description: '正在推送配置到设备...', + }, + success: { + title: '应用成功', + description: '配置已成功生效', + }, + error: { + title: '应用失败', + description: '请检查设备连接或配置内容', + }, + }); + } finally { + setApplying(false); } - setLoading(false); }; return ( @@ -52,57 +187,118 @@ const ConfigPage = () => { - - - {'交换机配置中心'} + + + 交换机配置中心 - + + + 选择交换机设备 + + setSelectedDevice(value[0] ?? '')} + placeholder={'请选择交换机设备'} + size={'sm'} + colorPalette={'teal'} + > + + + + + + + + + + + + + + {deviceCollection.items.map((item) => ( + + {item.label} + + ))} + + + + + -