From d398673f74851f018e5e34654d00fad0cba33e8e Mon Sep 17 00:00:00 2001 From: zhang1106 <849185023@qq.com> Date: Thu, 5 Feb 2026 10:32:10 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E7=B3=BB=E7=BB=9F?= =?UTF-8?q?=E8=AE=BE=E7=BD=AE=E4=B8=AD=E4=BF=AE=E6=94=B9=E5=89=8D=E7=AB=AF?= =?UTF-8?q?=E7=AB=AF=E5=8F=A3=E5=B9=B6=E8=87=AA=E5=8A=A8=E9=87=8D=E5=90=AF?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 后端:添加 frontend_port 配置项和重启API - 前端:添加端口设置表单和自动重启交互 - 添加前端服务管理脚本支持启动/停止/重启 - 优化重启流程,避免Network Error提示 --- backend/routes/systemSettings.js | 75 ++++++++ frontend/src/pages/SystemSettings.jsx | 114 ++++++++++++- frontend/vite.config.js | 31 +++- scripts/frontend-manager.js | 235 ++++++++++++++++++++++++++ 4 files changed, 449 insertions(+), 6 deletions(-) create mode 100644 scripts/frontend-manager.js diff --git a/backend/routes/systemSettings.js b/backend/routes/systemSettings.js index 21ef8b5..b0ddc81 100644 --- a/backend/routes/systemSettings.js +++ b/backend/routes/systemSettings.js @@ -16,6 +16,7 @@ const initDefaultSettings = async () => { { settingKey: 'session_timeout', settingValue: JSON.stringify(30), settingType: 'number', category: 'general', description: '会话超时时间(分钟)', isEditable: true }, { settingKey: 'max_login_attempts', settingValue: JSON.stringify(5), settingType: 'number', category: 'general', description: '最大登录尝试次数', isEditable: true }, { settingKey: 'maintenance_mode', settingValue: JSON.stringify(false), settingType: 'boolean', category: 'general', description: '维护模式', isEditable: true }, + { settingKey: 'frontend_port', settingValue: JSON.stringify(3000), settingType: 'number', category: 'general', description: '前端服务端口(修改后需重启前端服务)', isEditable: true }, // 外观设置 { settingKey: 'primary_color', settingValue: JSON.stringify('#667eea'), settingType: 'string', category: 'appearance', description: '主题主色调', isEditable: true }, @@ -227,6 +228,7 @@ router.post('/reset/:key', async (req, res) => { session_timeout: 30, max_login_attempts: 5, maintenance_mode: false, + frontend_port: 3000, primary_color: '#667eea', secondary_color: '#764ba2', compact_mode: false, @@ -518,4 +520,77 @@ router.get('/system/info', async (req, res) => { } }); +// 获取前端端口配置(供 vite.config.js 使用) +router.get('/frontend/port', async (req, res) => { + try { + const portSetting = await SystemSetting.findByPk('frontend_port'); + const port = portSetting ? JSON.parse(portSetting.settingValue) : 3000; + res.json({ port }); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +// 同步前端端口到配置文件(供前端保存设置后调用) +router.post('/frontend/port/sync', async (req, res) => { + try { + const fs = require('fs'); + const path = require('path'); + + const portSetting = await SystemSetting.findByPk('frontend_port'); + const port = portSetting ? JSON.parse(portSetting.settingValue) : 3000; + + // 写入前端配置文件 + const frontendDir = path.join(__dirname, '../../frontend'); + const configPath = path.join(frontendDir, '.frontend-port'); + + fs.writeFileSync(configPath, port.toString(), 'utf-8'); + + res.json({ + message: '前端端口配置已同步', + port, + configPath: '.frontend-port', + notice: '配置已更新,请重启前端服务以应用新端口' + }); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +// 重启前端服务 +router.post('/frontend/restart', async (req, res) => { + try { + const { restartFrontend, getStatus } = require('../../scripts/frontend-manager'); + + // 先获取当前状态 + const beforeStatus = await getStatus(); + + // 执行重启 + const result = await restartFrontend(); + + res.json({ + message: '前端服务重启成功', + before: beforeStatus, + after: { + pid: result.pid, + port: result.port, + url: `http://localhost:${result.port}` + } + }); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +// 获取前端服务状态 +router.get('/frontend/status', async (req, res) => { + try { + const { getStatus } = require('../../scripts/frontend-manager'); + const status = await getStatus(); + res.json(status); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + module.exports = router; diff --git a/frontend/src/pages/SystemSettings.jsx b/frontend/src/pages/SystemSettings.jsx index 483f0f6..9966819 100644 --- a/frontend/src/pages/SystemSettings.jsx +++ b/frontend/src/pages/SystemSettings.jsx @@ -13,14 +13,25 @@ const SystemSettings = () => { const [settings, setSettings] = useState({}); const [activeTab, setActiveTab] = useState('general'); const [systemInfo, setSystemInfo] = useState(null); + const [frontendStatus, setFrontendStatus] = useState(null); const [form] = Form.useForm(); const { reloadConfig } = useConfig(); useEffect(() => { fetchSettings(); fetchSystemInfo(); + fetchFrontendStatus(); }, []); + const fetchFrontendStatus = async () => { + try { + const response = await axios.get('/api/system-settings/frontend/status'); + setFrontendStatus(response.data); + } catch (error) { + console.error('获取前端服务状态失败:', error); + } + }; + const fetchSettings = async () => { setLoading(true); try { @@ -59,7 +70,80 @@ const SystemSettings = () => { }); await axios.put('/api/system-settings', { settings: updates }); - message.success('设置保存成功'); + + // 如果修改了前端端口,同步配置并自动重启 + if ('frontend_port' in updates) { + try { + // 1. 同步端口到配置文件 + await axios.post('/api/system-settings/frontend/port/sync'); + + // 2. 显示确认对话框 + Modal.confirm({ + title: '前端端口已修改', + icon: , + content: ( +
+

前端端口已从 {settings.frontend_port?.value} 更改为 {updates.frontend_port}

+

是否立即重启前端服务以应用新端口?

+ +
+ ), + okText: '立即重启', + cancelText: '稍后手动重启', + onOk: async () => { + const newPort = updates.frontend_port; + const newUrl = `http://localhost:${newPort}`; + + // 先显示跳转提示,再调用重启API + // 因为重启API会导致当前服务中断 + Modal.success({ + title: '正在重启前端服务', + content: ( +
+

前端服务正在重启,新端口:{newPort}

+

页面将在3秒后自动跳转到新地址...

+

如果跳转失败,请手动访问:{newUrl}

+
+ ), + okText: '立即跳转', + closable: false, + maskClosable: false, + onOk: () => { + window.location.href = newUrl; + } + }); + + // 延迟调用重启API,让用户看到提示 + setTimeout(async () => { + try { + // 调用重启API(这个请求可能会因为服务重启而失败) + await axios.post('/api/system-settings/frontend/restart', {}, { timeout: 5000 }); + } catch (error) { + // 忽略错误,因为服务重启会导致连接中断 + console.log('重启请求已发送,服务正在重启...'); + } + }, 1000); + + // 延迟3秒后跳转 + setTimeout(() => { + window.location.href = newUrl; + }, 3000); + } + }); + } catch (syncError) { + console.warn('同步前端端口配置失败:', syncError); + message.warning('端口配置已保存,但同步到配置文件失败'); + } + } else { + message.success('设置保存成功'); + } + fetchSettings(); // 重新加载全局配置,使更改立即生效 await reloadConfig(); @@ -115,14 +199,20 @@ const SystemSettings = () => { ); case 'number': + const isPortField = key === 'frontend_port'; return ( Number(value) } + ] : []) + ]} > - + ); case 'select': @@ -219,9 +309,25 @@ const SystemSettings = () => { }; const renderGeneralSettings = () => { - const generalKeys = ['site_name', 'site_logo', 'timezone', 'date_format', 'session_timeout', 'max_login_attempts', 'maintenance_mode']; + const generalKeys = ['site_name', 'site_logo', 'timezone', 'date_format', 'session_timeout', 'max_login_attempts', 'maintenance_mode', 'frontend_port']; return ( + {frontendStatus && ( + +

当前端口:{frontendStatus.port}

+

运行状态:{frontendStatus.isRunning ? 运行中 : 未运行}

+ {frontendStatus.portInUse &&

警告:端口被其他程序占用

} +

访问地址:{frontendStatus.url}

+ + } + type={frontendStatus.isRunning ? 'success' : 'warning'} + showIcon + style={{ marginBottom: 24 }} + /> + )}
{generalKeys.map(key => { // 确保时区和日期格式使用select类型 diff --git a/frontend/vite.config.js b/frontend/vite.config.js index 61292fd..0ee7a8d 100644 --- a/frontend/vite.config.js +++ b/frontend/vite.config.js @@ -1,12 +1,39 @@ import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; +import fs from 'fs'; +import path from 'path'; + +// 从配置文件或环境变量获取端口 +const getFrontendPort = () => { + // 优先级:环境变量 > 配置文件 > 默认值 + if (process.env.FRONTEND_PORT) { + return parseInt(process.env.FRONTEND_PORT, 10); + } + + // 尝试读取配置文件 + const configPath = path.resolve(__dirname, '.frontend-port'); + if (fs.existsSync(configPath)) { + try { + const port = parseInt(fs.readFileSync(configPath, 'utf-8').trim(), 10); + if (!isNaN(port) && port > 0 && port < 65536) { + return port; + } + } catch (e) { + console.warn('读取前端端口配置文件失败,使用默认端口 3000'); + } + } + + return 3000; +}; + +const port = getFrontendPort(); export default defineConfig({ plugins: [react()], assetsInclude: ['**/*.hdr', '**/*.woff2'], server: { host: '0.0.0.0', - port: 3000, + port: port, proxy: { '/api': { target: 'http://localhost:8000', @@ -46,4 +73,4 @@ export default defineConfig({ optimizeDeps: { include: ['antd', '@ant-design/icons', 'axios', 'react-router-dom'] } -}); \ No newline at end of file +}); diff --git a/scripts/frontend-manager.js b/scripts/frontend-manager.js new file mode 100644 index 0000000..e51bbe9 --- /dev/null +++ b/scripts/frontend-manager.js @@ -0,0 +1,235 @@ +/** + * 前端服务管理器 + * 用于在系统设置中修改端口后自动重启前端服务 + */ + +const { exec } = require('child_process'); +const fs = require('fs'); +const path = require('path'); +const net = require('net'); + +// 配置文件路径 +const CONFIG_FILE = path.join(__dirname, '../frontend/.frontend-port'); +const PID_FILE = path.join(__dirname, '../frontend/.frontend-pid'); + +// 检查端口是否被占用 +const checkPort = (port) => { + return new Promise((resolve) => { + const server = net.createServer(); + server.once('error', () => { + resolve(false); // 端口被占用 + }); + server.once('listening', () => { + server.close(); + resolve(true); // 端口可用 + }); + server.listen(port); + }); +}; + +// 获取当前配置的端口 +const getConfiguredPort = () => { + try { + if (fs.existsSync(CONFIG_FILE)) { + const port = parseInt(fs.readFileSync(CONFIG_FILE, 'utf-8').trim(), 10); + if (!isNaN(port) && port > 0 && port < 65536) { + return port; + } + } + } catch (e) { + console.warn('读取端口配置失败:', e.message); + } + return 3000; // 默认端口 +}; + +// 保存PID到文件 +const savePid = (pid) => { + try { + fs.writeFileSync(PID_FILE, pid.toString(), 'utf-8'); + } catch (e) { + console.error('保存PID失败:', e.message); + } +}; + +// 读取PID文件 +const getSavedPid = () => { + try { + if (fs.existsSync(PID_FILE)) { + const pid = parseInt(fs.readFileSync(PID_FILE, 'utf-8').trim(), 10); + return isNaN(pid) ? null : pid; + } + } catch (e) { + console.warn('读取PID文件失败:', e.message); + } + return null; +}; + +// 清除PID文件 +const clearPid = () => { + try { + if (fs.existsSync(PID_FILE)) { + fs.unlinkSync(PID_FILE); + } + } catch (e) { + console.warn('清除PID文件失败:', e.message); + } +}; + +// 检查进程是否存在 +const isProcessRunning = (pid) => { + try { + process.kill(pid, 0); + return true; + } catch (e) { + return false; + } +}; + +// 停止前端服务 +const stopFrontend = async () => { + return new Promise((resolve) => { + const pid = getSavedPid(); + + if (pid && isProcessRunning(pid)) { + console.log(`正在停止前端服务 (PID: ${pid})...`); + + // Windows 使用 taskkill + const killCmd = process.platform === 'win32' + ? `taskkill /PID ${pid} /T /F` + : `kill -9 ${pid}`; + + exec(killCmd, (error) => { + if (error) { + console.warn('停止服务警告:', error.message); + } else { + console.log('前端服务已停止'); + } + clearPid(); + resolve(); + }); + } else { + console.log('没有找到运行中的前端服务'); + clearPid(); + resolve(); + } + }); +}; + +// 启动前端服务 +const startFrontend = async () => { + const port = getConfiguredPort(); + + // 检查端口是否可用 + const isAvailable = await checkPort(port); + if (!isAvailable) { + throw new Error(`端口 ${port} 已被占用,请更换端口或关闭占用该端口的程序`); + } + + console.log(`正在启动前端服务,端口: ${port}...`); + + const frontendDir = path.join(__dirname, '../frontend'); + + // 使用 exec 启动服务(Windows 兼容) + const isWindows = process.platform === 'win32'; + const cmd = isWindows + ? `set FRONTEND_PORT=${port} && npm run dev` + : `FRONTEND_PORT=${port} npm run dev`; + + const child = exec(cmd, { + cwd: frontendDir, + windowsHide: true // Windows 隐藏窗口 + }); + + // 保存PID + savePid(child.pid); + console.log(`前端服务已启动 (PID: ${child.pid}),端口: ${port}`); + + return { pid: child.pid, port }; +}; + +// 重启前端服务 +const restartFrontend = async () => { + console.log('正在重启前端服务...'); + await stopFrontend(); + + // 等待1秒确保端口释放 + await new Promise(resolve => setTimeout(resolve, 1000)); + + const result = await startFrontend(); + console.log(`前端服务重启完成,访问地址: http://localhost:${result.port}`); + return result; +}; + +// 获取前端服务状态 +const getStatus = async () => { + const pid = getSavedPid(); + const port = getConfiguredPort(); + const isRunning = pid ? isProcessRunning(pid) : false; + + // 检查端口实际占用情况 + const portAvailable = await checkPort(port); + + return { + pid, + port, + isRunning, + portInUse: !portAvailable && !isRunning, // 端口被占用但服务未运行 + url: `http://localhost:${port}` + }; +}; + +// 命令行接口 +const main = async () => { + const command = process.argv[2]; + + try { + switch (command) { + case 'start': + await startFrontend(); + break; + case 'stop': + await stopFrontend(); + break; + case 'restart': + await restartFrontend(); + break; + case 'status': + const status = await getStatus(); + console.log('前端服务状态:'); + console.log(` PID: ${status.pid || '无'}`); + console.log(` 端口: ${status.port}`); + console.log(` 运行中: ${status.isRunning ? '是' : '否'}`); + console.log(` 访问地址: ${status.url}`); + if (status.portInUse) { + console.log(' 警告: 端口被其他程序占用'); + } + break; + default: + console.log('用法: node frontend-manager.js [start|stop|restart|status]'); + console.log(''); + console.log('命令:'); + console.log(' start - 启动前端服务'); + console.log(' stop - 停止前端服务'); + console.log(' restart - 重启前端服务'); + console.log(' status - 查看服务状态'); + process.exit(1); + } + } catch (error) { + console.error('错误:', error.message); + process.exit(1); + } +}; + +// 如果是直接运行此脚本 +if (require.main === module) { + main(); +} + +// 导出模块供其他文件使用 +module.exports = { + startFrontend, + stopFrontend, + restartFrontend, + getStatus, + getConfiguredPort +};