Files
yunrui_asset/frontend/src/pages/SystemSettings.jsx
T
zhang1106 d398673f74 feat: 添加系统设置中修改前端端口并自动重启功能
- 后端:添加 frontend_port 配置项和重启API
- 前端:添加端口设置表单和自动重启交互
- 添加前端服务管理脚本支持启动/停止/重启
- 优化重启流程,避免Network Error提示
2026-02-05 10:32:10 +08:00

469 lines
17 KiB
React
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useState, useEffect } from 'react';
import { Tabs, Form, Input, Switch, Select, Button, Card, Space, message, Modal, Tag, Divider, Descriptions, Alert } from 'antd';
import { SettingOutlined, GlobalOutlined, BgColorsOutlined, InfoCircleOutlined, CheckCircleOutlined, ExclamationCircleOutlined } from '@ant-design/icons';
import axios from 'axios';
import { useConfig } from '../context/ConfigContext';
const { Option } = Select;
const { TabPane } = Tabs;
const SystemSettings = () => {
const [loading, setLoading] = useState(false);
const [saving, setSaving] = useState(false);
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 {
const response = await axios.get('/api/system-settings');
setSettings(response.data);
const formValues = {};
Object.entries(response.data).forEach(([key, data]) => {
formValues[key] = data.value;
});
form.setFieldsValue(formValues);
} catch (error) {
message.error('获取设置失败');
} finally {
setLoading(false);
}
};
const fetchSystemInfo = async () => {
try {
const response = await axios.get('/api/system-settings/system/info');
setSystemInfo(response.data);
} catch (error) {
console.error('获取系统信息失败');
}
};
const handleSaveSettings = async (values) => {
setSaving(true);
try {
const updates = {};
Object.entries(values).forEach(([key, value]) => {
if (settings[key] && settings[key].isEditable) {
updates[key] = value;
}
});
await axios.put('/api/system-settings', { settings: updates });
// 如果修改了前端端口,同步配置并自动重启
if ('frontend_port' in updates) {
try {
// 1. 同步端口到配置文件
await axios.post('/api/system-settings/frontend/port/sync');
// 2. 显示确认对话框
Modal.confirm({
title: '前端端口已修改',
icon: <ExclamationCircleOutlined />,
content: (
<div>
<p>前端端口已从 <strong>{settings.frontend_port?.value}</strong> 更改为 <strong>{updates.frontend_port}</strong></p>
<p>是否立即重启前端服务以应用新端口</p>
<Alert
message="注意"
description="重启后页面将自动跳转到新地址,如果新端口无法访问,请手动使用原端口访问。"
type="warning"
showIcon
style={{ marginTop: 16 }}
/>
</div>
),
okText: '立即重启',
cancelText: '稍后手动重启',
onOk: async () => {
const newPort = updates.frontend_port;
const newUrl = `http://localhost:${newPort}`;
// 先显示跳转提示,再调用重启API
// 因为重启API会导致当前服务中断
Modal.success({
title: '正在重启前端服务',
content: (
<div>
<p>前端服务正在重启新端口<strong>{newPort}</strong></p>
<p>页面将在3秒后自动跳转到新地址...</p>
<p>如果跳转失败请手动访问<a href={newUrl}>{newUrl}</a></p>
</div>
),
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();
} catch (error) {
message.error('保存设置失败');
} finally {
setSaving(false);
}
};
const handleResetSetting = (key) => {
Modal.confirm({
title: '确认重置',
icon: <ExclamationCircleOutlined />,
content: `确定要将 "${settings[key]?.description || key}" 重置为默认值吗?`,
onOk: async () => {
try {
await axios.post(`/api/system-settings/reset/${key}`);
message.success('重置成功');
fetchSettings();
// 重新加载全局配置,使更改立即生效
await reloadConfig();
} catch (error) {
message.error('重置失败');
}
}
});
};
const renderFormItem = (key, data) => {
if (!data.isEditable) {
return (
<Form.Item
key={key}
label={data.description || key}
name={key}
>
<Input disabled suffix={<LockOutlined />} />
</Form.Item>
);
}
switch (data.type) {
case 'boolean':
return (
<Form.Item
key={key}
label={data.description || key}
name={key}
valuePropName="checked"
>
<Switch />
</Form.Item>
);
case 'number':
const isPortField = key === 'frontend_port';
return (
<Form.Item
key={key}
label={data.description || key}
name={key}
rules={[
{ required: false, message: `请输入${data.description || key}` },
...(isPortField ? [
{ type: 'number', min: 1, max: 65535, message: '端口号必须在 1-65535 之间', transform: value => Number(value) }
] : [])
]}
>
<Input type="number" style={{ width: '100%' }} min={isPortField ? 1 : undefined} max={isPortField ? 65535 : undefined} />
</Form.Item>
);
case 'select':
const options = getSelectOptions(key);
return (
<Form.Item
key={key}
label={data.description || key}
name={key}
>
<Select>
{options.map(opt => (
<Option key={opt.value} value={opt.value}>{opt.label}</Option>
))}
</Select>
</Form.Item>
);
default:
return (
<Form.Item
key={key}
label={data.description || key}
name={key}
>
<Input />
</Form.Item>
);
}
};
const getSelectOptions = (key) => {
const optionsMap = {
timezone: [
{ value: 'Asia/Shanghai', label: '亚洲/上海 (UTC+8)' },
{ value: 'Asia/Beijing', label: '亚洲/北京 (UTC+8)' },
{ value: 'America/New_York', label: '美洲/纽约 (UTC-5)' },
{ value: 'Europe/London', label: '欧洲/伦敦 (UTC+0)' },
{ value: 'UTC', label: 'UTC (UTC+0)' }
],
date_format: [
{ value: 'YYYY-MM-DD', label: '2024-01-01' },
{ value: 'YYYY/MM/DD', label: '2024/01/01' },
{ value: 'DD/MM/YYYY', label: '01/01/2024' },
{ value: 'MM/DD/YYYY', label: '01/01/2024' }
],
primary_color: [
{ value: '#667eea', label: '蓝色 (#667eea)' },
{ value: '#764ba2', label: '紫色 (#764ba2)' },
{ value: '#f093fb', label: '粉色 (#f093fb)' },
{ value: '#4facfe', label: '浅蓝色 (#4facfe)' },
{ value: '#43e97b', label: '绿色 (#43e97b)' },
{ value: '#fa709a', label: '红色 (#fa709a)' },
{ value: '#fee140', label: '黄色 (#fee140)' },
{ value: '#00b4db', label: '青色 (#00b4db)' },
{ value: '#0083b0', label: '深蓝色 (#0083b0)' },
{ value: '#fcb045', label: '橙色 (#fcb045)' }
],
secondary_color: [
{ value: '#764ba2', label: '紫色 (#764ba2)' },
{ value: '#667eea', label: '蓝色 (#667eea)' },
{ value: '#4facfe', label: '浅蓝色 (#4facfe)' },
{ value: '#43e97b', label: '绿色 (#43e97b)' },
{ value: '#fa709a', label: '红色 (#fa709a)' },
{ value: '#fee140', label: '黄色 (#fee140)' },
{ value: '#00b4db', label: '青色 (#00b4db)' },
{ value: '#0083b0', label: '深蓝色 (#0083b0)' },
{ value: '#fcb045', label: '橙色 (#fcb045)' },
{ value: '#f093fb', label: '粉色 (#f093fb)' }
],
table_row_height: [
{ value: 'small', label: '紧凑 (Small)' },
{ value: 'default', label: '默认 (Default)' },
{ value: 'middle', label: '中等 (Middle)' },
{ value: 'large', label: '宽松 (Large)' }
],
dark_mode: [
{ value: 'false', label: '关闭' },
{ value: 'true', label: '开启' }
],
compact_mode: [
{ value: 'false', label: '关闭' },
{ value: 'true', label: '开启' }
],
animation_enabled: [
{ value: 'false', label: '关闭' },
{ value: 'true', label: '开启' }
],
sidebar_collapsed: [
{ value: 'false', label: '展开' },
{ value: 'true', label: '折叠' }
],
};
return optionsMap[key] || [];
};
const renderGeneralSettings = () => {
const generalKeys = ['site_name', 'site_logo', 'timezone', 'date_format', 'session_timeout', 'max_login_attempts', 'maintenance_mode', 'frontend_port'];
return (
<Card title="全局配置" bordered={false}>
{frontendStatus && (
<Alert
message="前端服务状态"
description={
<div>
<p>当前端口<strong>{frontendStatus.port}</strong></p>
<p>运行状态{frontendStatus.isRunning ? <Tag color="success">运行中</Tag> : <Tag color="error">未运行</Tag>}</p>
{frontendStatus.portInUse && <p style={{ color: '#ff4d4f' }}>警告端口被其他程序占用</p>}
<p>访问地址<a href={frontendStatus.url} target="_blank" rel="noopener noreferrer">{frontendStatus.url}</a></p>
</div>
}
type={frontendStatus.isRunning ? 'success' : 'warning'}
showIcon
style={{ marginBottom: 24 }}
/>
)}
<Form form={form} layout="vertical" onFinish={handleSaveSettings}>
{generalKeys.map(key => {
// 确保时区和日期格式使用select类型
const settingData = { ...settings[key] };
if (key === 'timezone' || key === 'date_format') {
settingData.type = 'select';
}
return renderFormItem(key, settingData);
})}
<Form.Item>
<Space>
<Button type="primary" htmlType="submit" loading={saving}>保存设置</Button>
<Button onClick={() => fetchSettings()}>重置表单</Button>
</Space>
</Form.Item>
</Form>
</Card>
);
};
const renderAppearanceSettings = () => {
const appearanceKeys = ['primary_color', 'secondary_color', 'compact_mode', 'sidebar_collapsed', 'table_row_height', 'animation_enabled'];
return (
<Card title="外观设置" bordered={false}>
<Form form={form} layout="vertical" onFinish={handleSaveSettings}>
<Alert
message="主题颜色"
description="修改主题颜色后需要刷新页面才能生效。"
type="info"
showIcon
style={{ marginBottom: 16 }}
/>
{appearanceKeys.map(key => {
// 确保主题颜色使用select类型
const settingData = { ...settings[key] };
if (key === 'primary_color' || key === 'secondary_color') {
settingData.type = 'select';
}
return renderFormItem(key, settingData);
})}
<Form.Item>
<Space>
<Button type="primary" htmlType="submit" loading={saving}>保存设置</Button>
<Button onClick={() => fetchSettings()}>重置表单</Button>
</Space>
</Form.Item>
</Form>
</Card>
);
};
// 数据备份功能已移除
const renderAboutPage = () => {
const aboutKeys = ['app_version', 'company_name', 'contact_email', 'contact_phone', 'company_address', 'system_description', 'privacy_policy', 'terms_of_service'];
return (
<div>
<Card title="关于系统" bordered={false} style={{ marginBottom: 16 }}>
<Descriptions column={{ xs: 1, sm: 2, md: 3 }} bordered>
<Descriptions.Item label="系统名称">机柜管理系统</Descriptions.Item>
<Descriptions.Item label="版本号">{settings.app_version?.value || '1.0.0'}</Descriptions.Item>
<Descriptions.Item label="系统状态">
<Tag color="success">运行正常</Tag>
</Descriptions.Item>
</Descriptions>
</Card>
<Card title="公司信息" bordered={false} style={{ marginBottom: 16 }}>
<Form layout="vertical">
{aboutKeys.slice(1).map(key => settings[key] && renderFormItem(key, settings[key]))}
<Form.Item>
<Space>
<Button type="primary" htmlType="submit" loading={saving} onClick={() => form.submit()}>保存信息</Button>
<Button onClick={() => fetchSettings()}>重置</Button>
</Space>
</Form.Item>
</Form>
</Card>
{systemInfo && (
<Card title="系统统计信息" bordered={false}>
<Descriptions column={{ xs: 1, sm: 2, md: 4 }} bordered size="small">
<Descriptions.Item label="设备总数">{systemInfo.statistics?.devices || 0}</Descriptions.Item>
<Descriptions.Item label="机柜总数">{systemInfo.statistics?.racks || 0}</Descriptions.Item>
<Descriptions.Item label="机房总数">{systemInfo.statistics?.rooms || 0}</Descriptions.Item>
<Descriptions.Item label="用户总数">{systemInfo.statistics?.users || 0}</Descriptions.Item>
</Descriptions>
<Divider />
<Descriptions column={{ xs: 1, sm: 2 }} bordered size="small">
<Descriptions.Item label="Node.js 版本">{systemInfo.system?.nodeVersion}</Descriptions.Item>
<Descriptions.Item label="运行平台">{systemInfo.system?.platform} ({systemInfo.system?.arch})</Descriptions.Item>
<Descriptions.Item label="进程 ID">{systemInfo.system?.pid}</Descriptions.Item>
<Descriptions.Item label="运行时间">
{systemInfo.system?.uptime ? `${Math.floor(systemInfo.system.uptime / 3600)}小时${Math.floor((systemInfo.system.uptime % 3600) / 60)}分钟` : '-'}
</Descriptions.Item>
<Descriptions.Item label="内存使用">
{systemInfo.system?.memoryUsage ? `${(systemInfo.system.memoryUsage.heapUsed / 1024 / 1024).toFixed(2)} MB` : '-'}
</Descriptions.Item>
<Descriptions.Item label="系统时间">
{systemInfo.timestamp ? new Date(systemInfo.timestamp).toLocaleString('zh-CN') : '-'}
</Descriptions.Item>
</Descriptions>
</Card>
)}
</div>
);
};
return (
<div style={{ padding: 24 }}>
<Tabs activeKey={activeTab} onChange={setActiveTab}>
<TabPane
tab={<span><GlobalOutlined /> 全局配置</span>}
key="general"
>
{renderGeneralSettings()}
</TabPane>
<TabPane
tab={<span><BgColorsOutlined /> 外观设置</span>}
key="appearance"
>
{renderAppearanceSettings()}
</TabPane>
<TabPane
tab={<span><InfoCircleOutlined /> 关于</span>}
key="about"
>
{renderAboutPage()}
</TabPane>
</Tabs>
</div>
);
};
import { LockOutlined } from '@ant-design/icons';
export default SystemSettings;