feat(桌面): DHCP+静态IP
parent
c3c8f11939
commit
7ac0003942
|
@ -20,6 +20,7 @@ export default defineConfig({
|
|||
"@assets": path.resolve(rootdir, "src/assets"),
|
||||
"@components": path.resolve(rootdir, "src/components"),
|
||||
"@utils": path.resolve(rootdir, "src/utils"),
|
||||
"@types": path.resolve(rootdir, "src/types"),
|
||||
},
|
||||
// 路由配置
|
||||
routes: [
|
||||
|
@ -43,6 +44,10 @@ export default defineConfig({
|
|||
path: '/login',
|
||||
component: '@/pages/login',
|
||||
},
|
||||
{
|
||||
path: '/configSteps',
|
||||
component: '@/pages/configSteps',
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
redirect: '/welcome',
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
export default {
|
||||
'POST /api/v1/sendMessage': (req,res)=>{
|
||||
res.send({
|
||||
code: 200,
|
||||
data: '发送成功'
|
||||
})
|
||||
}
|
||||
}
|
|
@ -12,6 +12,7 @@
|
|||
"@ant-design/icons": "^6.0.0",
|
||||
"antd": "^5.26.6",
|
||||
"axios": "^1.11.0",
|
||||
"classnames": "^2.5.1",
|
||||
"umi": "^4.0.42"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
"@ant-design/icons": "^6.0.0",
|
||||
"antd": "^5.26.6",
|
||||
"axios": "^1.11.0",
|
||||
"classnames": "^2.5.1",
|
||||
"umi": "^4.0.42"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 287 B |
|
@ -1,4 +1,8 @@
|
|||
import { ipcMain,app } from 'electron';
|
||||
import { getDeviceId, getWiredConnectionName,netmaskToCidr } from '../utils/utils';
|
||||
const { exec } = require('child_process');
|
||||
const { promisify } = require('util');
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
const window = getBrowserWindowRuntime();
|
||||
|
||||
|
@ -20,4 +24,67 @@ ipcMain.on('exit-kiosk', () => {
|
|||
if (window) {
|
||||
window.setFullScreen(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
ipcMain.handle('get-deviceid',async()=>{
|
||||
const deviceId = await getDeviceId();
|
||||
console.log(`Using device ID: ${deviceId}`);
|
||||
// TODO:传给后端
|
||||
})
|
||||
|
||||
/* IPC 处理应用有线网络配置 */
|
||||
ipcMain.handle('apply-wired-config',async(event,config)=>{
|
||||
// return {
|
||||
// success: true,
|
||||
// message: '网络配置已成功应用'
|
||||
// };
|
||||
try{
|
||||
console.log('应用网络配置:', config);
|
||||
// 获取有线连接名称
|
||||
const connectionName = await getWiredConnectionName();
|
||||
console.log('有线连接名称:', connectionName);
|
||||
|
||||
if(config.method==='static'){
|
||||
// 使用nmcli配置静态IP,需要使用sudo权限,一次性设置所有参数
|
||||
let modifyCmd = `echo "unis@123" | sudo -S nmcli connection modify "${connectionName}" ipv4.method manual ipv4.addresses "${config.ipv4}/${netmaskToCidr(config.subnetMask)}" ipv4.gateway "${config.ipv4Gateway}"`;
|
||||
const dnsServers = [config.primaryDns, config.secondaryDns].filter(Boolean).join(',');
|
||||
modifyCmd += ` ipv4.dns "${dnsServers}"`;
|
||||
|
||||
// 添加 IPv6 配置(如果存在 ipv6Gateway)????ipv6和长度需要吗?ui只写了ipv6网关
|
||||
// ipv6PrefixLength 是 IPv6 地址的前缀长度,类似于 IPv4 中的子网掩码。????
|
||||
if (config.ipv6 && config.ipv6Gateway) {
|
||||
modifyCmd += ` ipv6.method manual ipv6.addresses "${config.ipv6}/${config.ipv6PrefixLength || 64}" ipv6.gateway "${config.ipv6Gateway}"`;
|
||||
}
|
||||
|
||||
// 执行配置命令
|
||||
console.log('执行命令:', modifyCmd.replace('unis@123', '***'));
|
||||
await execAsync(modifyCmd);
|
||||
|
||||
// 重新激活连接
|
||||
await execAsync(`echo "unis@123" | sudo -S nmcli connection up "${connectionName}"`);
|
||||
|
||||
}else{
|
||||
// DHCP配置,一次性设置所有参数
|
||||
const modifyCmd = `echo "unis@123" | sudo -S nmcli connection modify "${connectionName}" ipv4.method auto ipv4.addresses "" ipv4.gateway "" ipv4.dns ""`;
|
||||
|
||||
// 执行配置命令
|
||||
console.log('执行命令:', modifyCmd.replace('unis@123', '***'));
|
||||
await execAsync(modifyCmd);
|
||||
|
||||
// 重新激活连接
|
||||
await execAsync(`echo "unis@123" | sudo -S nmcli connection up "${connectionName}"`);
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
message: '网络配置已成功应用'
|
||||
};
|
||||
}catch(error:unknown){
|
||||
console.error('应用网络配置失败:', error);
|
||||
return {
|
||||
success: false,
|
||||
message: `配置失败: ${error instanceof Error ? error.message : String(error || '未知错误')}`
|
||||
};
|
||||
}
|
||||
})
|
|
@ -0,0 +1,102 @@
|
|||
|
||||
const { exec } = require('child_process');
|
||||
const os = require('os');
|
||||
const { promisify } = require('util');
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
/** 获取设备ID(芯片序列号)
|
||||
* TODO: 增加获取mac地址的逻辑,需要传给后端
|
||||
*/
|
||||
export async function getDeviceId() {
|
||||
try {
|
||||
// 尝试多种方法获取唯一的设备标识
|
||||
const methods = [
|
||||
// 方法1: CPU序列号
|
||||
'cat /proc/cpuinfo | grep Serial | head -1 | awk \'{print $3}\'',
|
||||
// 方法2: 机器ID
|
||||
'cat /etc/machine-id 2>/dev/null || echo ""',
|
||||
// 方法3: DMI产品UUID
|
||||
'cat /sys/class/dmi/id/product_uuid 2>/dev/null || echo ""',
|
||||
// 方法4: 主板序列号
|
||||
'cat /sys/class/dmi/id/board_serial 2>/dev/null || echo ""'
|
||||
];
|
||||
|
||||
for (const command of methods) {
|
||||
try {
|
||||
const { stdout } = await execAsync(command);
|
||||
const deviceId = stdout.trim();
|
||||
|
||||
if (deviceId && deviceId !== '' && deviceId !== 'unknown' && deviceId !== '0000000000000000') {
|
||||
console.log(`Device ID obtained using command: ${command}`);
|
||||
console.log(`Device ID: ${deviceId}`);
|
||||
return deviceId;
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(`Method failed: ${command}, error: ${(error as Error).message}`);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果所有方法都失败,生成一个基于MAC地址的fallback ID
|
||||
const networkInterfaces = os.networkInterfaces();
|
||||
for (const interfaceName of Object.keys(networkInterfaces)) {
|
||||
const interfaces = networkInterfaces[interfaceName];
|
||||
for (const iface of interfaces) {
|
||||
if (iface.mac && iface.mac !== '00:00:00:00:00:00') {
|
||||
const fallbackId = iface.mac.replace(/:/g, '').toUpperCase();
|
||||
console.log(`Using MAC address as fallback device ID: ${fallbackId}`);
|
||||
return fallbackId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 最后的fallback - 使用hostname
|
||||
const hostname = os.hostname();
|
||||
console.log(`Using hostname as final fallback device ID: ${hostname}`);
|
||||
return hostname;
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error getting device ID:', error);
|
||||
// 返回一个默认的设备ID
|
||||
return 'UNKNOWN_DEVICE';
|
||||
}
|
||||
}
|
||||
|
||||
/** 获取有线网络连接名称 */
|
||||
export async function getWiredConnectionName() {
|
||||
try {
|
||||
// 首先尝试获取活动的有线连接
|
||||
const { stdout: activeConn } = await execAsync('nmcli -t -f NAME,TYPE connection show --active | grep ethernet | head -1 | cut -d: -f1');
|
||||
if (activeConn.trim()) {
|
||||
return activeConn.trim();
|
||||
}
|
||||
|
||||
// 如果没有活动连接,获取所有有线连接
|
||||
const { stdout: allConn } = await execAsync('nmcli -t -f NAME,TYPE connection show | grep ethernet | head -1 | cut -d: -f1');
|
||||
if (allConn.trim()) {
|
||||
return allConn.trim();
|
||||
}
|
||||
|
||||
// 默认连接名称
|
||||
return 'Wired connection 1';
|
||||
} catch (error) {
|
||||
console.error('获取有线连接名称失败:', error);
|
||||
return 'Wired connection 1';
|
||||
}
|
||||
}
|
||||
|
||||
/* 子网掩码转CIDR */
|
||||
export function netmaskToCidr(netmask:string) {
|
||||
const netmaskMap: { [key: string]: string } = {
|
||||
'255.255.255.0': '24',
|
||||
'255.255.0.0': '16',
|
||||
'255.0.0.0': '8',
|
||||
'255.255.255.128': '25',
|
||||
'255.255.255.192': '26',
|
||||
'255.255.255.224': '27',
|
||||
'255.255.255.240': '28',
|
||||
'255.255.255.248': '29',
|
||||
'255.255.255.252': '30'
|
||||
};
|
||||
return netmaskMap[netmask] || '24';
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
.button-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 15px 0;
|
||||
flex-shrink: 0;
|
||||
gap: 40px;
|
||||
|
||||
.cancel-button {
|
||||
width: 140px;
|
||||
height: 64px;
|
||||
border-radius: 32px;
|
||||
background: transparent;
|
||||
border: 1px solid rgba(134, 133, 158, 1);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
span {
|
||||
font-family: PingFang SC;
|
||||
font-weight: 400;
|
||||
font-style: Heavy;
|
||||
font-size: 20px;
|
||||
line-height: 32px;
|
||||
letter-spacing: 0%;
|
||||
color: rgba(229, 229, 229, 1);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
}
|
||||
|
||||
.confirm-button {
|
||||
height: 64px;
|
||||
border-radius: 32px;
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 30px;
|
||||
|
||||
span {
|
||||
font-family: PingFang SC;
|
||||
font-weight: 400;
|
||||
font-style: Heavy;
|
||||
font-size: 20px;
|
||||
line-height: 32px;
|
||||
letter-spacing: 0%;
|
||||
color: rgba(229, 229, 229, 1);
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.arrow-icon {
|
||||
width: 20px;
|
||||
height: 8px;
|
||||
background-image: url('../../../assets/stepIcon.png');
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import styles from './index.less';
|
||||
|
||||
interface ButtonComProps {
|
||||
// 取消按钮配置
|
||||
cancelText?: string;
|
||||
onCancel?: () => void;
|
||||
showCancel?: boolean;
|
||||
|
||||
// 确认按钮配置
|
||||
confirmText?: string;
|
||||
onConfirm?: () => void;
|
||||
showConfirm?: boolean;
|
||||
|
||||
// 样式类名
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const ButtonCom: React.FC<ButtonComProps> = ({
|
||||
cancelText = '取消',
|
||||
onCancel,
|
||||
showCancel = true,
|
||||
confirmText = '确认',
|
||||
onConfirm,
|
||||
showConfirm = true,
|
||||
className
|
||||
}) => {
|
||||
return (
|
||||
<div className={classNames(styles["button-container"], className)}>
|
||||
{showCancel && (
|
||||
<button
|
||||
className={styles["cancel-button"]}
|
||||
onClick={onCancel}
|
||||
>
|
||||
<span>{cancelText}</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{showConfirm && (
|
||||
<button
|
||||
className={styles["confirm-button"]}
|
||||
onClick={onConfirm}
|
||||
>
|
||||
<span>{confirmText}</span>
|
||||
<div className={styles["arrow-icon"]}></div>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ButtonCom;
|
|
@ -1,9 +1,12 @@
|
|||
.main-layout {
|
||||
min-height: 100vh;
|
||||
background-color: rgba(0, 9, 51, 0.9);
|
||||
}
|
||||
|
||||
|
||||
.main-content {
|
||||
min-height: 100vh;
|
||||
// background-size: 100% 100%;
|
||||
background-color: rgba(0, 9, 51, 0.9);
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
}
|
||||
|
|
@ -23,11 +23,21 @@ const MainLayout: React.FC = () => {
|
|||
// setUsername(currentUsername || '');
|
||||
// }, []);
|
||||
|
||||
useEffect(() => {
|
||||
useEffect(() => {
|
||||
// TODO: 第一次来:判断是否配置ip/DHCP、服务ip绑定终端 绑定:直接到版本更新页面 未绑定:到配置ip/DHCP页面
|
||||
setTimeout(() => {
|
||||
history.push('/login');
|
||||
},9000)
|
||||
// setTimeout(() => {
|
||||
// history.push('/configSteps');
|
||||
// },1000)
|
||||
// const fetchDeviceId = async () => {
|
||||
// try {
|
||||
// const res = await window.electronAPI.invoke('get-deviceid');
|
||||
// console.log('获取设备ID:', res);
|
||||
// } catch (error) {
|
||||
// console.error('获取设备ID失败:', error);
|
||||
// }
|
||||
// }
|
||||
// fetchDeviceId()
|
||||
|
||||
}, []);
|
||||
|
||||
const handleMenuClick = (key: string) => {
|
||||
|
@ -43,11 +53,11 @@ const MainLayout: React.FC = () => {
|
|||
};
|
||||
|
||||
return (
|
||||
<Layout className="main-layout">
|
||||
<Content className="main-content">
|
||||
<div className="main-layout">
|
||||
<div className="main-content">
|
||||
<Outlet />
|
||||
</Content>
|
||||
</Layout>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -0,0 +1,178 @@
|
|||
.network-config {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.tab-container {
|
||||
height: 70px;
|
||||
flex-shrink: 0;
|
||||
background: linear-gradient(180deg, rgba(229, 229, 229, 0.05) 0%, rgba(229, 229, 229, 0.05) 100%);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
.tab-item {
|
||||
font-family: PingFang SC;
|
||||
font-weight: 400;
|
||||
font-style: Heavy;
|
||||
top: -5px;
|
||||
font-size: 20px;
|
||||
line-height: 30px;
|
||||
letter-spacing: 0%;
|
||||
padding: 0 30px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
color: rgba(229, 229, 229, 0.5);
|
||||
|
||||
&.active {
|
||||
color: rgba(229, 229, 229, 1);
|
||||
}
|
||||
|
||||
.indicator {
|
||||
position: absolute;
|
||||
bottom: -15px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: rgba(229, 229, 229, 1);
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.content-container {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.dhcp-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
|
||||
h2 {
|
||||
font-family: PingFang SC;
|
||||
font-weight: 400;
|
||||
font-style: Heavy;
|
||||
font-size: 24px;
|
||||
color: rgba(229, 229, 229, 1);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
p {
|
||||
font-family: PingFang SC;
|
||||
font-size: 18px;
|
||||
color: rgba(229, 229, 229, 0.8);
|
||||
}
|
||||
}
|
||||
|
||||
.static-ip-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
|
||||
.form-container {
|
||||
width: 500px;
|
||||
margin: 0 auto;
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 20px 0px;
|
||||
padding-right: 60px;
|
||||
|
||||
// 滚动条样式
|
||||
&::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-radius: 28px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 28px;
|
||||
min-height: 30px;
|
||||
}
|
||||
|
||||
// 表单项样式
|
||||
.ant-form-item {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.ant-form-item-label {
|
||||
padding: 0 0 12px 0;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-family: PingFang SC;
|
||||
font-weight: 400;
|
||||
font-style: Heavy;
|
||||
font-size: 18px;
|
||||
line-height: 32px;
|
||||
letter-spacing: 0%;
|
||||
color: rgba(229, 229, 229, 1);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.input-field {
|
||||
width: 100%;
|
||||
height: 56px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 28px;
|
||||
border: none;
|
||||
padding: 0 24px;
|
||||
box-sizing: border-box;
|
||||
font-family: PingFang SC;
|
||||
font-weight: 400;
|
||||
font-style: Heavy;
|
||||
font-size: 18px;
|
||||
line-height: 32px;
|
||||
letter-spacing: 0%;
|
||||
color: rgba(229, 229, 229, 1);
|
||||
|
||||
&::placeholder {
|
||||
color: rgba(229, 229, 229, 0.5);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
}
|
||||
|
||||
// 覆盖 Ant Design 的默认样式
|
||||
.ant-input {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 28px;
|
||||
border: none;
|
||||
padding: 0 24px;
|
||||
box-sizing: border-box;
|
||||
font-family: PingFang SC;
|
||||
font-weight: 400;
|
||||
font-style: Heavy;
|
||||
font-size: 18px;
|
||||
line-height: 32px;
|
||||
letter-spacing: 0%;
|
||||
color: rgba(229, 229, 229, 1);
|
||||
height: 56px;
|
||||
|
||||
&::placeholder {
|
||||
color: rgba(229, 229, 229, 0.5);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -0,0 +1,182 @@
|
|||
import React, { useState } from 'react';
|
||||
import styles from './index.less';
|
||||
import cs from 'classnames';
|
||||
import { Form, Input, message } from 'antd';
|
||||
import ButtonCom from '../../../components/ButtonCom';
|
||||
|
||||
const staticIpFormFields: CONFIG_STEPS.StaticFormFieldConfig[] = [
|
||||
{
|
||||
name: "ipv4",
|
||||
label: "IPv4",
|
||||
type: "input",
|
||||
placeholder: "请输入",
|
||||
rules: [
|
||||
{ required: true, message: '请输入IPv4地址' },
|
||||
{
|
||||
pattern: /^((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]?)$/,
|
||||
message: '请输入正确的IPv4地址格式'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "subnetMask",
|
||||
label: "子网掩码",
|
||||
type: "input",
|
||||
placeholder: "请输入",
|
||||
rules: [
|
||||
{ required: true, message: '请输入子网掩码' },
|
||||
{
|
||||
pattern: /^((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]?)$/,
|
||||
message: '请输入正确的子网掩码格式'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "ipv4Gateway",
|
||||
label: "IPv4网关",
|
||||
type: "input",
|
||||
placeholder: "请输入",
|
||||
rules: [
|
||||
{ required: true, message: '请输入IPv4网关' },
|
||||
{
|
||||
pattern: /^((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]?)$/,
|
||||
message: '请输入正确的IPv4网关格式'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "ipv6Gateway",
|
||||
label: "IPv6网关",
|
||||
type: "input",
|
||||
placeholder: "请输入",
|
||||
rules: [
|
||||
{
|
||||
pattern: /^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$|^([0-9a-fA-F]{1,4}:)*::([0-9a-fA-F]{1,4}:)*[0-9a-fA-F]{1,4}$/,
|
||||
message: '请输入正确的IPv6地址格式'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "primaryDns",
|
||||
label: "首选DNS",
|
||||
type: "input",
|
||||
placeholder: "请输入",
|
||||
rules: [
|
||||
{ required: true, message: '请输入首选DNS' },
|
||||
{
|
||||
pattern: /^((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]?)$/,
|
||||
message: '请输入正确的DNS地址格式'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "secondaryDns",
|
||||
label: "备用DNS",
|
||||
type: "input",
|
||||
placeholder: "请输入",
|
||||
rules: [
|
||||
{
|
||||
pattern: /^((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]?)$/,
|
||||
message: '请输入正确的DNS地址格式'
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
const NetworkConfig: React.FC = () => {
|
||||
const [activeTab, setActiveTab] = useState<'dhcp' | 'static'>('dhcp');
|
||||
const [form] = Form.useForm();
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (activeTab === 'dhcp') {
|
||||
// 如果是 DHCP 模式,直接IPC 处理应用有线网络配置
|
||||
try {
|
||||
const res = await window.electronAPI.invoke('apply-wired-config',{ method: 'dhcp' });
|
||||
console.log('网络配置返回信息成功:', res);
|
||||
if(res.success){
|
||||
message.success('网络配置成功');
|
||||
}else{
|
||||
message.error(res.message || '网络配置失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('网络配置返回信息失败:', error);
|
||||
}
|
||||
} else {
|
||||
// 如果是静态IP模式,进行表单校验,再IPC 处理应用有线网络配置
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
console.log('表单提交数据:', values);
|
||||
const res = await window.electronAPI.invoke('apply-wired-config',{ method: 'static', ...values });
|
||||
console.log('网络配置返回信息成功:', res);
|
||||
if(res.success){
|
||||
message.success('网络配置成功');
|
||||
}else{
|
||||
message.error(res.message || '网络配置失败');
|
||||
}
|
||||
} catch (errorInfo) {
|
||||
console.log('网络配置返回信息失败:', errorInfo);
|
||||
}
|
||||
}
|
||||
// TODO: 处理网络配置成功后的逻辑,跳转到下一个步骤或页面
|
||||
};
|
||||
|
||||
const DhcpComponent = () => (
|
||||
<div className={styles["dhcp-content"]}>
|
||||
<h2>DHCP 配置</h2>
|
||||
<p>正在使用 DHCP 自动获取网络配置</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
const StaticIpComponent = () => (
|
||||
<div className={styles["static-ip-container"]}>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
requiredMark={false}
|
||||
className={styles["form-container"]}
|
||||
>
|
||||
{staticIpFormFields.map(field => (
|
||||
<Form.Item
|
||||
key={field.name}
|
||||
name={field.name}
|
||||
label={<div className={styles["label"]}>{field.label}</div>}
|
||||
rules={field.rules}
|
||||
>
|
||||
<Input
|
||||
className={styles["input-field"]}
|
||||
placeholder={field.placeholder}
|
||||
/>
|
||||
</Form.Item>
|
||||
))}
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={styles["network-config"]}>
|
||||
<div className={styles["tab-container"]}>
|
||||
<div
|
||||
className={cs(styles["tab-item"], { [styles.active]: activeTab === 'dhcp' })}
|
||||
onClick={() => setActiveTab('dhcp')}
|
||||
>
|
||||
<span>DHCP</span>
|
||||
{activeTab === 'dhcp' && <div className={styles["indicator"]}></div>}
|
||||
</div>
|
||||
<div
|
||||
className={cs(styles["tab-item"], { [styles.active]: activeTab === 'static' })}
|
||||
onClick={() => setActiveTab('static')}
|
||||
>
|
||||
<span>静态IP</span>
|
||||
{activeTab === 'static' && <div className={styles["indicator"]}></div>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles["content-container"]}>
|
||||
{activeTab === 'dhcp' ? <DhcpComponent /> : <StaticIpComponent />}
|
||||
</div>
|
||||
<ButtonCom confirmText="确认并进入下一步" onConfirm={handleSubmit}/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default NetworkConfig;
|
|
@ -0,0 +1,11 @@
|
|||
import React from 'react';
|
||||
|
||||
const Index = () => {
|
||||
return (
|
||||
<div>
|
||||
终端获取镜像信息
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Index;
|
|
@ -0,0 +1,11 @@
|
|||
import React from 'react';
|
||||
|
||||
const Index = () => {
|
||||
return (
|
||||
<div>
|
||||
侦测管理平台
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Index;
|
|
@ -0,0 +1,70 @@
|
|||
.config-step-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding-top: 24px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.tabs-container {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 60px;
|
||||
flex-shrink: 0;
|
||||
// background: rgba(229, 229, 229, 0.2);
|
||||
|
||||
.tab-item {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
|
||||
.tab-label {
|
||||
font-size: 22px;
|
||||
color: rgba(255, 255, 255, 0.6); // 未激活tab文字透明
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
.tab-indicator {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
height: 4px;
|
||||
background: rgba(229, 229, 229, 0.2);
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
&.active {
|
||||
.tab-label {
|
||||
color: white; // 激活tab文字为白色
|
||||
}
|
||||
|
||||
.tab-indicator {
|
||||
background: white; // 激活tab指示器为白色
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tab-content-container {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
|
||||
.tab-content {
|
||||
height: 100%;
|
||||
max-height: 100%;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 4px;
|
||||
padding: 20px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
|
||||
.emptyBox{
|
||||
height: 60px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
// src/pages/configSteps/index.tsx
|
||||
import React, { useState } from 'react';
|
||||
import styles from './index.less';
|
||||
import cs from 'classnames';
|
||||
import NetworkConfig from './components/networkConfig';
|
||||
import WatchManagement from './components/watchManagement';
|
||||
import TerminalGetImage from './components/terminalGetImage';
|
||||
|
||||
const Index: React.FC = () => {
|
||||
const [activeTab, setActiveTab] = useState<string>("networkConfig");
|
||||
|
||||
const tabs = [
|
||||
{ key: "networkConfig", label: '平台网络配置', component: <NetworkConfig /> },
|
||||
{ key: "watchManagement", label: '侦测管理平台', component: <WatchManagement /> },
|
||||
{ key: "terminalGetImage", label: '终端获取镜像信息', component: <TerminalGetImage /> },
|
||||
];
|
||||
|
||||
const activeTabItem = tabs.find(tab => tab.key === activeTab);
|
||||
|
||||
return (
|
||||
<div className={styles["config-step-container"]}>
|
||||
<div className={styles["tabs-container"]}>
|
||||
{tabs.map((tab) => (
|
||||
<div
|
||||
key={tab.key}
|
||||
className={cs(styles["tab-item"], {
|
||||
[styles.active]: activeTab === tab.key
|
||||
})}
|
||||
onClick={() => setActiveTab(tab.key)}
|
||||
>
|
||||
<span className={styles["tab-label"]}>{tab.label}</span>
|
||||
<div className={styles["tab-indicator"]} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className={styles["tab-content-container"]}>
|
||||
{activeTabItem?.component || <div>未找到对应内容</div>}
|
||||
</div>
|
||||
<div className={styles.emptyBox}></div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Index;
|
|
@ -1,8 +1,6 @@
|
|||
.welcomeCon{
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background-color: rgba(0, 9, 51, 1);
|
||||
background-size: 100% 100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import axios from '@/utils/axios';
|
||||
import axios from '@utils/axios';
|
||||
|
||||
export const BASE_URL = '/api/v1/test';
|
||||
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
declare namespace CONFIG_STEPS {
|
||||
interface StaticFormFieldConfig {
|
||||
name: string;
|
||||
label: string;
|
||||
type: 'input' | 'select'; // 可扩展更多类型
|
||||
rules?: any[];
|
||||
placeholder?: string;
|
||||
options?: { label: string; value: string }[]; // select 专用
|
||||
required?: boolean;
|
||||
}
|
||||
}
|
|
@ -1,3 +1,3 @@
|
|||
{
|
||||
"extends": "./src/.umi/tsconfig.json"
|
||||
"extends": "./src/.umi/tsconfig.json",
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue