diff --git a/src/frontend/src/components/pages/welcome/ConnectionConfigModal.jsx b/src/frontend/src/components/pages/welcome/ConnectionConfigModal.jsx index 11e2fa6..c319126 100644 --- a/src/frontend/src/components/pages/welcome/ConnectionConfigModal.jsx +++ b/src/frontend/src/components/pages/welcome/ConnectionConfigModal.jsx @@ -18,12 +18,9 @@ import { import { motion, AnimatePresence } from 'framer-motion'; import { IoClose } from 'react-icons/io5'; import { FiCheck } from 'react-icons/fi'; +import ConfigTool, { defaultConfig } from '@/libs/config/ConfigTool'; const MotionBox = motion(Box); -const defaultConfig = { - backendUrl: '', - authKey: '', -}; /** * 连接地址配置组件 @@ -39,10 +36,7 @@ const ConnectionConfigModal = ({ isOpen, onClose, onSave }) => { const backendRef = useRef(null); useEffect(() => { - const savedData = localStorage.getItem('connection-config'); - if (savedData) { - setConfig(JSON.parse(savedData)); - } + setConfig(ConfigTool.load()); }, []); const handleChange = (e) => { @@ -51,7 +45,7 @@ const ConnectionConfigModal = ({ isOpen, onClose, onSave }) => { }; const handleSave = () => { - localStorage.setItem('connection-config', JSON.stringify(config)); + ConfigTool.save(config); onSave(config); setSaved(true); setTimeout(() => { @@ -61,8 +55,9 @@ const ConnectionConfigModal = ({ isOpen, onClose, onSave }) => { }; const handleClear = () => { - localStorage.removeItem('connection-config'); + ConfigTool.clear(); setConfig(defaultConfig); + onClose(); }; return ( @@ -155,8 +150,8 @@ const ConnectionConfigModal = ({ isOpen, onClose, onSave }) => { color={'white'} onClick={handleSave} isDisabled={saved} - position={'relative'} width={'80px'} + position={'relative'} _hover={{ bg: 'rgba(0, 0, 255, 0.3)' }} > diff --git a/src/frontend/src/components/pages/welcome/WelcomeContent.jsx b/src/frontend/src/components/pages/welcome/WelcomeContent.jsx index 1beb393..c5b4245 100644 --- a/src/frontend/src/components/pages/welcome/WelcomeContent.jsx +++ b/src/frontend/src/components/pages/welcome/WelcomeContent.jsx @@ -4,6 +4,7 @@ import FadeInWrapper from '@/components/system/layout/FadeInWrapper'; import { useState, useEffect } from 'react'; import ConnectionConfigModal from '@/components/pages/welcome/ConnectionConfigModal'; import ConfigureCard from '@/components/pages/welcome/ConfigureCard'; +import ConfigTool from '@/libs/config/ConfigTool'; /** * 欢迎页面内容 @@ -16,7 +17,7 @@ const WelcomeContent = () => { //const [showAlert, setShowAlert] = useState(false); useEffect(() => { - const saved = localStorage.getItem('connection-config'); + const saved = ConfigTool.load(); if (saved) { setIsConfigured(true); } diff --git a/src/frontend/src/libs/config/ConfigTool.js b/src/frontend/src/libs/config/ConfigTool.js new file mode 100644 index 0000000..adda66c --- /dev/null +++ b/src/frontend/src/libs/config/ConfigTool.js @@ -0,0 +1,85 @@ +const CONFIG_KEY = 'app_config'; + +export const defaultConfig = { + backendUrl: '', + authKey: '', +}; + +/** + * 浏览器环境配置操作工具 + */ +const ConfigTool = { + /** + * 从本地存储读取配置 + * @returns {{backendUrl: string, authKey: string}} + */ + load: () => { + try { + const stored = localStorage.getItem(CONFIG_KEY); + if (stored) { + return { ...defaultConfig, ...JSON.parse(stored) }; + } + } catch (e) { + console.error('读取配置失败:', e); + localStorage.removeItem(CONFIG_KEY); + } + return { ...defaultConfig }; + }, + + /** + * 保存配置到本地存储 + * @param {Object} config - 要保存的配置对象 + * @returns {boolean} 是否保存成功 + */ + save: (config) => { + try { + localStorage.setItem(CONFIG_KEY, JSON.stringify(config)); + //console.log(`正在保存配置:${JSON.stringify(config)}`); + //console.log(`测试读取配置:${JSON.stringify(ConfigTool.load())}`); + return true; + } catch (e) { + console.error('保存配置失败:', e); + if (e.name === 'QuotaExceededError') { + alert('存储空间不足,请清理后重试'); + } + return false; + } + }, + + /** + * 清除配置 + * @returns {boolean} 是否清除成功 + */ + clear: () => { + try { + localStorage.removeItem(CONFIG_KEY); + return true; + } catch (e) { + console.error('清除配置失败:', e); + return false; + } + }, + + /** + * 获取存储标识(浏览器环境返回固定值) + * @returns {string} + */ + getConfigPath: () => `localStorage:${CONFIG_KEY}`, + + /** + * 检查存储是否可用 + * @returns {boolean} + */ + isStorageAvailable: () => { + try { + const testKey = 'test'; + localStorage.setItem(testKey, testKey); + localStorage.removeItem(testKey); + return true; + } catch (e) { + return false; + } + }, +}; + +export default ConfigTool; diff --git a/src/frontend/src/libs/system/Notification.jsx b/src/frontend/src/libs/system/Notification.jsx index 701f35f..ae772e0 100644 --- a/src/frontend/src/libs/system/Notification.jsx +++ b/src/frontend/src/libs/system/Notification.jsx @@ -1,5 +1,5 @@ import React, { createContext, useContext, useState, useCallback } from 'react'; -import { Box, Alert, Button, Icon, Text } from '@chakra-ui/react'; +import { Box, Button, Icon, Text } from '@chakra-ui/react'; import { AnimatePresence, motion } from 'framer-motion'; import { AiOutlineInfoCircle, AiFillWarning } from 'react-icons/ai'; diff --git a/src/frontend/src/pages/ScanPage.jsx b/src/frontend/src/pages/ScanPage.jsx index 508493b..6e9fdb7 100644 --- a/src/frontend/src/pages/ScanPage.jsx +++ b/src/frontend/src/pages/ScanPage.jsx @@ -15,7 +15,7 @@ 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 { api } from '@/services/api'; +import { api } from '@/services/api/api'; import { useNotification } from '@/libs/system/Notification'; /** @@ -32,18 +32,10 @@ const ScanPage = () => { useEffect(() => { const fetchLocalInfo = async () => { - try { - const res = await api.test(); - if (res.message) { - setLocalIp(res.local_ip || '172.17.99.208'); - if (!subnet) { - const ipParts = (res.local_ip || '172.17.99.208').split('.'); - setSubnet(`${ipParts[0]}.${ipParts[1]}.${ipParts[2]}.0/24`); - } - } - } catch (err) { - setLocalIp('未知'); - notify.error({ title: '获取后端信息失败' }); + setLocalIp('172.17.99.208'); + if (!subnet) { + const ipParts = '172.17.99.208'.split('.'); + setSubnet(`${ipParts[0]}.${ipParts[1]}.${ipParts[2]}.0/24`); } }; fetchLocalInfo(); diff --git a/src/frontend/src/services/api.js b/src/frontend/src/services/api.js deleted file mode 100644 index 3b051fe..0000000 --- a/src/frontend/src/services/api.js +++ /dev/null @@ -1,51 +0,0 @@ -/** - * 获取config - * @returns {any|null} - */ -export const getConfig = () => { - const cfg = localStorage.getItem('connection-config'); - return cfg ? JSON.parse(cfg) : null; -}; - -/** - * 获取后端url - * @returns {string} - */ -export const getBaseUrl = () => { - const cfg = getConfig(); - return cfg?.backendUrl || ''; -}; - -/** - * fetchUrl - * @param path 路径 - * @param options 选项 - * @returns {Promise} - */ -const fetchWithBase = async (path, options = {}) => { - const base = getBaseUrl(); - const res = await fetch(`${base}${path}`, options); - return res.json(); -}; - -/** - * api模块 - * @type {{test: (function(): Promise<*>), scan: (function(*): Promise<*>), listDevices: (function(): Promise<*>), parseCommand: (function(*): Promise<*>), applyConfig: (function(*, *): Promise<*>)}} - */ -export const api = { - test: () => fetchWithBase('/api/test'), - scan: (subnet) => fetchWithBase(`/api/scan_network?subnet=${encodeURIComponent(subnet)}`), - listDevices: () => fetchWithBase('/api/list_devices'), - parseCommand: (text) => - fetchWithBase('/api/parse_command', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ command: text }), - }), - applyConfig: (switch_ip, config) => - fetchWithBase('/api/apply_config', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ switch_ip, config }), - }), -}; diff --git a/src/frontend/src/services/api/api.js b/src/frontend/src/services/api/api.js new file mode 100644 index 0000000..47a9b84 --- /dev/null +++ b/src/frontend/src/services/api/api.js @@ -0,0 +1,100 @@ +import axios from 'axios'; +import ConfigTool from '@/libs/config/ConfigTool'; + +/** + * 创建带基础URL的axios实例 + */ +const apiClient = axios.create({ + baseURL: ConfigTool.load().backendUrl || '', + timeout: 10000, + headers: { + 'Content-Type': 'application/json', + }, +}); + +// 请求拦截器 +apiClient.interceptors.request.use( + (config) => { + const cfg = ConfigTool.load(); + if (cfg?.authKey) { + config.headers['Authorization'] = `Bearer ${cfg.authKey}`; + } + return config; + }, + (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模块 + */ +export const api = { + /** + * 测试API连接 + * @returns {Promise>} + */ + test: () => apiClient.get('/api/test'), + + /** + * 扫描网络 + * @param subnet 子网地址 + * @returns {Promise>} + */ + scan: (subnet) => apiClient.get(`/api/scan_network`, { params: { subnet } }), + + /** + * 列出所有设备 + * @returns {Promise>} + */ + listDevices: () => apiClient.get('/api/list_devices'), + + /** + * 解析命令 + * @param text 文本 + * @returns {Promise>} + */ + parseCommand: (text) => apiClient.post('/api/parse_command', { command: text }), + + /** + * 应用配置 + * @param switch_ip 交换机ip + * @param config 配置 + * @returns {Promise>} + */ + applyConfig: (switch_ip, config) => apiClient.post('/api/apply_config', { switch_ip, config }), + + /** + * 更新基础URL + * @param url + */ + updateBaseUrl: (url) => { + const config = ConfigTool.load(); + config.backendUrl = url; + ConfigTool.save(config); + apiClient.defaults.baseURL = url; + }, +}; + +/** + * 获取当前配置 + * @returns {{backendUrl: string, authKey: string}} + */ +export const getConfig = () => ConfigTool.load(); + +/** + * 获取基础URL + * @returns {string|string} + */ +export const getBaseUrl = () => ConfigTool.load().backendUrl || ''; + +export default apiClient;