You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
701 lines
25 KiB
701 lines
25 KiB
import React, { useEffect } from "react";
|
|
import {
|
|
Card,
|
|
Input,
|
|
Slider,
|
|
InputNumber,
|
|
Switch,
|
|
Select,
|
|
Row,
|
|
Col,
|
|
Button,
|
|
message,
|
|
Space,
|
|
Typography,
|
|
Spin,
|
|
Alert,
|
|
} from "antd";
|
|
import {
|
|
SettingOutlined,
|
|
SaveOutlined,
|
|
DatabaseOutlined,
|
|
ClockCircleOutlined,
|
|
BellOutlined,
|
|
FilterOutlined,
|
|
CloudUploadOutlined,
|
|
SyncOutlined,
|
|
} from "@ant-design/icons";
|
|
import useAdvancedSettings from "../hooks/useAdvancedSettings";
|
|
|
|
const { Option } = Select;
|
|
const { Title, Text } = Typography;
|
|
|
|
const AdvancedSettings = ({ onLogout }) => {
|
|
// 使用高级配置 Hook
|
|
const {
|
|
settings,
|
|
loading,
|
|
isConnected,
|
|
isReady,
|
|
fetchAllSettings,
|
|
saveAllSettings,
|
|
resetSettings,
|
|
updateLocalSettings,
|
|
} = useAdvancedSettings();
|
|
|
|
// 只在首次 isReady 变为 true 时自动拉取配置
|
|
const hasFetchedRef = React.useRef(false);
|
|
useEffect(() => {
|
|
if (isReady && !hasFetchedRef.current) {
|
|
fetchAllSettings();
|
|
hasFetchedRef.current = true;
|
|
}
|
|
}, [isReady, fetchAllSettings]);
|
|
|
|
// IP 地址验证函数
|
|
const validateIP = (ip) => {
|
|
if (!ip) return true; // 允许空值
|
|
const ipRegex = /^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
|
|
|
|
// 如果匹配 IP 格式(包含点分十进制),则严格按 IP 验证
|
|
if (/^\d+\.\d+\.\d+\.\d+$/.test(ip)) {
|
|
return ipRegex.test(ip);
|
|
}
|
|
|
|
// 否则按域名验证
|
|
const domainRegex = /^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;
|
|
return domainRegex.test(ip);
|
|
};
|
|
|
|
// 端口验证函数
|
|
const validatePort = (port) => {
|
|
const portNum = Number(port);
|
|
return !isNaN(portNum) && portNum >= 1 && portNum <= 65535;
|
|
};
|
|
|
|
// 从 settings 中提取配置
|
|
const deviceId = settings.deviceId;
|
|
const fps = settings.dataFps;
|
|
const enableOfflineAlert = settings.alertConfig?.enable ?? false;
|
|
const offlineThreshold = settings.alertConfig?.intervalSec ?? 60;
|
|
const enableFiltering = settings.filterConfig?.enable ?? true;
|
|
const filterMethod = settings.filterConfig?.method ?? 'median';
|
|
const windowSize = settings.filterConfig?.size ?? 5;
|
|
const filterThreshold = settings.filterConfig?.threshold ?? 0.1;
|
|
const flowThreshold = settings.filterConfig?.imgThreshold ?? 10.0;
|
|
const enableMqtt = settings.mqttConfig?.enable ?? true;
|
|
const brokerAddress = settings.mqttConfig?.mqtt?.broker ?? '';
|
|
const mqttPort = settings.mqttConfig?.mqtt?.port ?? 1883;
|
|
const mqttTopic = settings.mqttConfig?.mqtt?.topic ?? '';
|
|
const mqttClientId = settings.mqttConfig?.mqtt?.client_id ?? '';
|
|
const mqttUsername = settings.mqttConfig?.mqtt?.username ?? '';
|
|
const mqttPassword = settings.mqttConfig?.mqtt?.password ?? '';
|
|
|
|
// 更新本地状态的辅助函数
|
|
const setDeviceId = (value) => updateLocalSettings({ deviceId: value });
|
|
const setFps = (value) => updateLocalSettings({ dataFps: value });
|
|
const setEnableOfflineAlert = (value) =>
|
|
updateLocalSettings({
|
|
alertConfig: {
|
|
enable: value,
|
|
intervalSec: settings.alertConfig?.intervalSec ?? 60
|
|
},
|
|
});
|
|
const setOfflineThreshold = (value) =>
|
|
updateLocalSettings({
|
|
alertConfig: {
|
|
enable: settings.alertConfig?.enable ?? false,
|
|
intervalSec: value
|
|
},
|
|
});
|
|
const setEnableFiltering = (value) =>
|
|
updateLocalSettings({
|
|
filterConfig: { ...settings.filterConfig, enable: value },
|
|
});
|
|
const setFilterMethod = (value) =>
|
|
updateLocalSettings({
|
|
filterConfig: { ...settings.filterConfig, method: value },
|
|
});
|
|
const setWindowSize = (value) =>
|
|
updateLocalSettings({
|
|
filterConfig: { ...settings.filterConfig, size: value },
|
|
});
|
|
const setFilterThreshold = (value) =>
|
|
updateLocalSettings({
|
|
filterConfig: { ...settings.filterConfig, threshold: -Math.abs(value) },
|
|
});
|
|
const setFlowThreshold = (value) =>
|
|
updateLocalSettings({
|
|
filterConfig: { ...settings.filterConfig, imgThreshold: value },
|
|
});
|
|
const setEnableMqtt = (value) =>
|
|
updateLocalSettings({
|
|
mqttConfig: {
|
|
enable: value,
|
|
mqtt: settings.mqttConfig?.mqtt ?? {
|
|
broker: '',
|
|
port: 1883,
|
|
topic: '',
|
|
username: '',
|
|
password: '',
|
|
client_id: ''
|
|
}
|
|
},
|
|
});
|
|
const setBrokerAddress = (value) =>
|
|
updateLocalSettings({
|
|
mqttConfig: {
|
|
...settings.mqttConfig,
|
|
mqtt: {
|
|
...(settings.mqttConfig?.mqtt ?? {}),
|
|
broker: value
|
|
},
|
|
},
|
|
});
|
|
const setMqttPort = (value) =>
|
|
updateLocalSettings({
|
|
mqttConfig: {
|
|
...settings.mqttConfig,
|
|
mqtt: {
|
|
...(settings.mqttConfig?.mqtt ?? {}),
|
|
port: value
|
|
},
|
|
},
|
|
});
|
|
const setMqttTopic = (value) =>
|
|
updateLocalSettings({
|
|
mqttConfig: {
|
|
...settings.mqttConfig,
|
|
mqtt: {
|
|
...(settings.mqttConfig?.mqtt ?? {}),
|
|
topic: value
|
|
},
|
|
},
|
|
});
|
|
const setMqttClientId = (value) =>
|
|
updateLocalSettings({
|
|
mqttConfig: {
|
|
...settings.mqttConfig,
|
|
mqtt: {
|
|
...(settings.mqttConfig?.mqtt ?? {}),
|
|
client_id: value
|
|
},
|
|
},
|
|
});
|
|
const setMqttUsername = (value) =>
|
|
updateLocalSettings({
|
|
mqttConfig: {
|
|
...settings.mqttConfig,
|
|
mqtt: {
|
|
...(settings.mqttConfig?.mqtt ?? {}),
|
|
username: value
|
|
},
|
|
},
|
|
});
|
|
const setMqttPassword = (value) =>
|
|
updateLocalSettings({
|
|
mqttConfig: {
|
|
...settings.mqttConfig,
|
|
mqtt: {
|
|
...(settings.mqttConfig?.mqtt ?? {}),
|
|
password: value
|
|
},
|
|
},
|
|
});
|
|
|
|
const [saving, setSaving] = React.useState(false);
|
|
|
|
const handleSave = async () => {
|
|
if (enableMqtt) {
|
|
if (brokerAddress && !validateIP(brokerAddress)) {
|
|
message.error('Broker 地址格式不正确,请检查后重试');
|
|
return;
|
|
}
|
|
if (mqttPort && !validatePort(mqttPort)) {
|
|
message.error('端口号格式不正确,请检查后重试');
|
|
return;
|
|
}
|
|
if (!brokerAddress) {
|
|
message.warning('启用 MQTT 时请填写 Broker 地址');
|
|
return;
|
|
}
|
|
}
|
|
|
|
try {
|
|
setSaving(true);
|
|
const success = await saveAllSettings();
|
|
|
|
if (!success) {
|
|
console.log('保存操作未完全成功');
|
|
}
|
|
} catch (error) {
|
|
console.error('保存配置时发生异常:', error);
|
|
message.error('保存配置时发生异常,请重试');
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
};
|
|
|
|
const handleReset = () => {
|
|
fetchAllSettings();
|
|
};
|
|
|
|
return (
|
|
<Spin spinning={loading || saving} tip={saving ? "正在保存配置..." : "正在加载配置..."}>
|
|
<div
|
|
style={{
|
|
padding: "12px",
|
|
backgroundColor: "#f5f7fa",
|
|
minHeight: "calc(100vh - 92px)",
|
|
}}
|
|
>
|
|
{/* WebSocket 连接状态提示 */}
|
|
{!isConnected && (
|
|
<Alert
|
|
message="WebSocket 未连接"
|
|
description="当前未连接到设备,无法读取或保存配置。请检查网络连接。"
|
|
type="warning"
|
|
showIcon
|
|
style={{ marginBottom: 12, borderRadius: 8 }}
|
|
/>
|
|
)}
|
|
|
|
<Card
|
|
style={{
|
|
marginBottom: 12,
|
|
background:
|
|
"linear-gradient(135deg, #01152cff 0%, #063b77ff 100%)",
|
|
borderRadius: 12,
|
|
}}
|
|
styles={{ padding: 12 }}
|
|
>
|
|
<Row justify="space-between" align="middle">
|
|
<Col>
|
|
<Space direction="vertical" size={4}>
|
|
<Title
|
|
level={2}
|
|
style={{
|
|
margin: 0,
|
|
color: "white",
|
|
display: "flex",
|
|
alignItems: "center",
|
|
gap: 12,
|
|
}}
|
|
>
|
|
<SettingOutlined />
|
|
高级参数配置
|
|
</Title>
|
|
<Text
|
|
style={{
|
|
color: "rgba(255,255,255,0.85)",
|
|
fontSize: 14,
|
|
}}
|
|
>
|
|
针对下位机运行的参数配置,修改后请及时保存(慎用)
|
|
</Text>
|
|
|
|
</Space>
|
|
</Col>
|
|
<Col>
|
|
<Space size="middle">
|
|
<Button
|
|
icon={<SyncOutlined spin={loading} />}
|
|
onClick={handleReset}
|
|
size="large"
|
|
disabled={!isConnected || loading}
|
|
style={{ height: 42, borderRadius: 8 }}
|
|
>
|
|
刷新配置
|
|
</Button>
|
|
<Button
|
|
type="primary"
|
|
icon={<SaveOutlined />}
|
|
onClick={handleSave}
|
|
size="large"
|
|
disabled={!isConnected || loading || saving}
|
|
loading={saving}
|
|
style={{
|
|
height: 42,
|
|
borderRadius: 8,
|
|
background: "white",
|
|
color: "#667eea",
|
|
borderColor: "white",
|
|
}}
|
|
>
|
|
{saving ? '保存中...' : '保存更改'}
|
|
</Button>
|
|
</Space>
|
|
</Col>
|
|
</Row>
|
|
</Card>
|
|
|
|
<Row gutter={12}>
|
|
<Col xs={12} lg={12}>
|
|
<Card
|
|
title={
|
|
<Space>
|
|
<DatabaseOutlined style={{ color: "#667eea" }} />
|
|
<span>基础设备信息</span>
|
|
</Space>
|
|
}
|
|
|
|
style={{
|
|
marginBottom: 12,
|
|
borderRadius: 12,
|
|
paddingLeft: 24,
|
|
paddingRight: 24,
|
|
}}
|
|
>
|
|
<div style={{ marginBottom: 4, fontWeight: 500 }}>
|
|
设备编码
|
|
</div>
|
|
<Input
|
|
value={deviceId}
|
|
onChange={(e) => setDeviceId(e.target.value)}
|
|
placeholder="请输入设备编码"
|
|
size="large"
|
|
style={{ borderRadius: 8 }}
|
|
/>
|
|
</Card>
|
|
|
|
<Card
|
|
title={
|
|
<Space>
|
|
<ClockCircleOutlined style={{ color: "#667eea" }} />
|
|
<span>数据帧率 (FPS)</span>
|
|
</Space>
|
|
}
|
|
|
|
style={{
|
|
marginBottom: 12,
|
|
borderRadius: 12,
|
|
paddingLeft: 24,
|
|
paddingRight: 24,
|
|
}}
|
|
>
|
|
<div style={{ marginBottom: 12, fontWeight: 500 }}>
|
|
数据帧率
|
|
</div>
|
|
<Row gutter={12} align="middle">
|
|
<Col span={18}>
|
|
<Slider
|
|
min={1}
|
|
max={30}
|
|
value={fps}
|
|
onChange={setFps}
|
|
marks={{ 1: "1 Hz", 30: "30 Hz" }}
|
|
/>
|
|
</Col>
|
|
<Col span={6}>
|
|
<InputNumber
|
|
min={1}
|
|
max={30}
|
|
value={fps}
|
|
onChange={setFps}
|
|
style={{ width: "100%", borderRadius: 8 }}
|
|
size="large"
|
|
/>
|
|
</Col>
|
|
</Row>
|
|
<Text type="secondary" style={{ fontSize: 12 }}>
|
|
有效范围 1~30 Hz
|
|
</Text>
|
|
</Card>
|
|
|
|
<Card
|
|
title={
|
|
<Space>
|
|
<BellOutlined style={{ color: "#667eea" }} />
|
|
<span>异常监控与报警</span>
|
|
</Space>
|
|
}
|
|
|
|
style={{
|
|
marginBottom: 12,
|
|
borderRadius: 12,
|
|
paddingLeft: 24,
|
|
paddingRight: 24,
|
|
}}
|
|
>
|
|
<div
|
|
style={{
|
|
background:
|
|
"linear-gradient(135deg, #f5f7fa 0%, #e8eef5 100%)",
|
|
padding: 20,
|
|
borderRadius: 12,
|
|
marginBottom: 12,
|
|
}}
|
|
>
|
|
<Row justify="space-between" align="middle">
|
|
<Col>
|
|
<Text strong>启用离线超时告警</Text>
|
|
<br />
|
|
<Text type="secondary" style={{ fontSize: 12 }}>
|
|
系统将按您设置的间隔周期持续检测:若连续离线时长达到整个周期,则会触发告警
|
|
</Text>
|
|
</Col>
|
|
<Col>
|
|
<Switch
|
|
checked={enableOfflineAlert}
|
|
onChange={setEnableOfflineAlert}
|
|
/>
|
|
</Col>
|
|
</Row>
|
|
</div>
|
|
<div style={{ marginBottom: 4, fontWeight: 500 }}>
|
|
超时阈值
|
|
</div>
|
|
<InputNumber
|
|
min={10}
|
|
max={300}
|
|
value={offlineThreshold}
|
|
onChange={setOfflineThreshold}
|
|
size="large"
|
|
addonAfter="秒"
|
|
style={{ width: "100%", borderRadius: 8 }}
|
|
/>
|
|
</Card>
|
|
</Col>
|
|
|
|
<Col xs={12} lg={12}>
|
|
<Card
|
|
title={
|
|
<Space>
|
|
<FilterOutlined style={{ color: "#667eea" }} />
|
|
<span>数据滤波与异常记录</span>
|
|
</Space>
|
|
}
|
|
|
|
style={{
|
|
marginBottom: 12,
|
|
borderRadius: 12,
|
|
paddingLeft: 24,
|
|
paddingRight: 24,
|
|
}}
|
|
>
|
|
<div
|
|
style={{
|
|
background:
|
|
"linear-gradient(135deg, #f5f7fa 0%, #e8eef5 100%)",
|
|
padding: 20,
|
|
borderRadius: 12,
|
|
marginBottom: 20,
|
|
}}
|
|
>
|
|
<Row justify="space-between" align="middle">
|
|
<Col>
|
|
<Text strong>滤波配置</Text>
|
|
<br />
|
|
<Text type="secondary" style={{ fontSize: 12 }}>
|
|
启用数据滤波算法
|
|
</Text>
|
|
</Col>
|
|
<Col>
|
|
<Switch
|
|
checked={enableFiltering}
|
|
onChange={setEnableFiltering}
|
|
checkedChildren="已启用"
|
|
unCheckedChildren="已停用"
|
|
/>
|
|
</Col>
|
|
</Row>
|
|
</div>
|
|
<Row gutter={[12, 12]}>
|
|
<Col span={12}>
|
|
<div style={{ marginBottom: 4, fontWeight: 500 }}>
|
|
滤波方法
|
|
</div>
|
|
<Select
|
|
value={filterMethod}
|
|
onChange={setFilterMethod}
|
|
size="large"
|
|
disabled={!enableFiltering}
|
|
style={{ width: "100%", borderRadius: 8 }}
|
|
>
|
|
<Option value="median">中值滤波</Option>
|
|
</Select>
|
|
</Col>
|
|
<Col span={12}>
|
|
<div style={{ marginBottom: 4, fontWeight: 500 }}>
|
|
窗口大小
|
|
</div>
|
|
<InputNumber
|
|
min={3}
|
|
max={21}
|
|
step={2}
|
|
value={windowSize}
|
|
onChange={setWindowSize}
|
|
size="large"
|
|
disabled={!enableFiltering}
|
|
style={{ width: "100%", borderRadius: 8 }}
|
|
/>
|
|
</Col>
|
|
<Col span={12}>
|
|
<div style={{ marginBottom: 4, fontWeight: 500 }}>
|
|
滤波阈值
|
|
</div>
|
|
<InputNumber
|
|
value={filterThreshold}
|
|
onChange={setFilterThreshold}
|
|
addonAfter="mm"
|
|
size="large"
|
|
disabled={!enableFiltering}
|
|
style={{ width: "100%", borderRadius: 8 }}
|
|
/>
|
|
</Col>
|
|
<Col span={12}>
|
|
<div style={{ marginBottom: 4, fontWeight: 500 }}>
|
|
波动阈值
|
|
</div>
|
|
<InputNumber
|
|
min={0}
|
|
max={100}
|
|
step={0.1}
|
|
value={flowThreshold}
|
|
onChange={setFlowThreshold}
|
|
addonAfter="mm"
|
|
size="large"
|
|
disabled={!enableFiltering}
|
|
style={{ width: "100%", borderRadius: 8 }}
|
|
/>
|
|
</Col>
|
|
</Row>
|
|
</Card>
|
|
|
|
<Card
|
|
title={
|
|
<div
|
|
style={{
|
|
display: "flex",
|
|
justifyContent: "space-between",
|
|
alignItems: "center",
|
|
}}
|
|
>
|
|
<Space>
|
|
<CloudUploadOutlined style={{ color: "#667eea" }} />
|
|
<span>数据上报 (MQTT)</span>
|
|
</Space>
|
|
<Switch
|
|
checked={enableMqtt}
|
|
onChange={setEnableMqtt}
|
|
checkedChildren="启用"
|
|
unCheckedChildren="禁用"
|
|
/>
|
|
</div>
|
|
}
|
|
|
|
style={{
|
|
borderRadius: 12,
|
|
paddingLeft: 24,
|
|
paddingRight: 24,
|
|
}}
|
|
>
|
|
<Row gutter={[12, 12]}>
|
|
<Col span={12}>
|
|
<div style={{ marginBottom: 4, fontWeight: 500 }}>
|
|
Broker Address
|
|
<Text type="danger"> *</Text>
|
|
</div>
|
|
<Input
|
|
value={brokerAddress}
|
|
onChange={(e) => setBrokerAddress(e.target.value)}
|
|
placeholder="例如: 218.3.126.49 或 mqtt.example.com"
|
|
size="large"
|
|
disabled={!enableMqtt}
|
|
style={{
|
|
borderRadius: 8,
|
|
borderColor: brokerAddress && !validateIP(brokerAddress) ? '#ff4d4f' : undefined
|
|
}}
|
|
status={brokerAddress && !validateIP(brokerAddress) ? 'error' : undefined}
|
|
/>
|
|
{brokerAddress && !validateIP(brokerAddress) && (
|
|
<Text type="danger" style={{ fontSize: 12 }}>
|
|
请输入有效的 IPv4 地址
|
|
</Text>
|
|
)}
|
|
</Col>
|
|
<Col span={8}>
|
|
<div style={{ marginBottom: 4, fontWeight: 500 }}>
|
|
Port
|
|
<Text type="danger"> *</Text>
|
|
</div>
|
|
<Input
|
|
value={mqttPort}
|
|
onChange={(e) => setMqttPort(e.target.value)}
|
|
placeholder="1883"
|
|
size="large"
|
|
disabled={!enableMqtt}
|
|
style={{
|
|
borderRadius: 8,
|
|
borderColor: mqttPort && !validatePort(mqttPort) ? '#ff4d4f' : undefined
|
|
}}
|
|
status={mqttPort && !validatePort(mqttPort) ? 'error' : undefined}
|
|
/>
|
|
{mqttPort && !validatePort(mqttPort) && (
|
|
<Text type="danger" style={{ fontSize: 12 }}>
|
|
端口范围: 1-65535
|
|
</Text>
|
|
)}
|
|
</Col>
|
|
<Col span={12}>
|
|
<div style={{ marginBottom: 4, fontWeight: 500 }}>
|
|
Topic
|
|
</div>
|
|
<Input
|
|
value={mqttTopic}
|
|
onChange={(e) => setMqttTopic(e.target.value)}
|
|
placeholder="例如: wybb/z/mqtt179"
|
|
size="large"
|
|
disabled={!enableMqtt}
|
|
style={{ borderRadius: 8 }}
|
|
/>
|
|
</Col>
|
|
<Col span={8}>
|
|
<div style={{ marginBottom: 4, fontWeight: 500 }}>
|
|
Client ID
|
|
</div>
|
|
<Input
|
|
value={mqttClientId}
|
|
onChange={(e) => setMqttClientId(e.target.value)}
|
|
placeholder="wybb_z1_123"
|
|
size="large"
|
|
disabled={!enableMqtt}
|
|
style={{ borderRadius: 8 }}
|
|
/>
|
|
</Col>
|
|
<Col span={8}>
|
|
<div style={{ marginBottom: 4, fontWeight: 500 }}>
|
|
Username
|
|
</div>
|
|
<Input
|
|
value={mqttUsername}
|
|
onChange={(e) => setMqttUsername(e.target.value)}
|
|
placeholder="用户名"
|
|
size="large"
|
|
disabled={!enableMqtt}
|
|
style={{ borderRadius: 8 }}
|
|
/>
|
|
</Col>
|
|
<Col span={8}>
|
|
<div style={{ marginBottom: 4, fontWeight: 500 }}>
|
|
Password
|
|
</div>
|
|
<Input.Password
|
|
value={mqttPassword}
|
|
onChange={(e) => setMqttPassword(e.target.value)}
|
|
placeholder="密码"
|
|
size="large"
|
|
disabled={!enableMqtt}
|
|
style={{ borderRadius: 8 }}
|
|
/>
|
|
</Col>
|
|
</Row>
|
|
</Card>
|
|
</Col>
|
|
</Row>
|
|
</div>
|
|
</Spin>
|
|
);
|
|
};
|
|
|
|
export default AdvancedSettings;
|
|
|