feat(桌面): DHCP+静态IP

master
chenyt 2025-08-22 18:59:43 +08:00
parent c3c8f11939
commit 7ac0003942
23 changed files with 839 additions and 17 deletions

View File

@ -20,6 +20,7 @@ export default defineConfig({
"@assets": path.resolve(rootdir, "src/assets"), "@assets": path.resolve(rootdir, "src/assets"),
"@components": path.resolve(rootdir, "src/components"), "@components": path.resolve(rootdir, "src/components"),
"@utils": path.resolve(rootdir, "src/utils"), "@utils": path.resolve(rootdir, "src/utils"),
"@types": path.resolve(rootdir, "src/types"),
}, },
// 路由配置 // 路由配置
routes: [ routes: [
@ -43,6 +44,10 @@ export default defineConfig({
path: '/login', path: '/login',
component: '@/pages/login', component: '@/pages/login',
}, },
{
path: '/configSteps',
component: '@/pages/configSteps',
},
{ {
path: '/', path: '/',
redirect: '/welcome', redirect: '/welcome',

View File

@ -0,0 +1,8 @@
export default {
'POST /api/v1/sendMessage': (req,res)=>{
res.send({
code: 200,
data: '发送成功'
})
}
}

View File

@ -12,6 +12,7 @@
"@ant-design/icons": "^6.0.0", "@ant-design/icons": "^6.0.0",
"antd": "^5.26.6", "antd": "^5.26.6",
"axios": "^1.11.0", "axios": "^1.11.0",
"classnames": "^2.5.1",
"umi": "^4.0.42" "umi": "^4.0.42"
}, },
"devDependencies": { "devDependencies": {

View File

@ -19,6 +19,7 @@
"@ant-design/icons": "^6.0.0", "@ant-design/icons": "^6.0.0",
"antd": "^5.26.6", "antd": "^5.26.6",
"axios": "^1.11.0", "axios": "^1.11.0",
"classnames": "^2.5.1",
"umi": "^4.0.42" "umi": "^4.0.42"
}, },
"devDependencies": { "devDependencies": {

Binary file not shown.

After

Width:  |  Height:  |  Size: 287 B

View File

@ -1,4 +1,8 @@
import { ipcMain,app } from 'electron'; 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(); const window = getBrowserWindowRuntime();
@ -20,4 +24,67 @@ ipcMain.on('exit-kiosk', () => {
if (window) { if (window) {
window.setFullScreen(false); 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 || '未知错误')}`
};
}
})

View File

@ -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';
}

View File

@ -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);
}
}
}

View File

@ -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;

View File

@ -1,9 +1,12 @@
.main-layout { .main-layout {
min-height: 100vh; background-color: rgba(0, 9, 51, 0.9);
} }
.main-content { .main-content {
min-height: 100vh; // background-size: 100% 100%;
background-color: rgba(0, 9, 51, 0.9);
width: 100vw;
height: 100vh;
} }

View File

@ -23,11 +23,21 @@ const MainLayout: React.FC = () => {
// setUsername(currentUsername || ''); // setUsername(currentUsername || '');
// }, []); // }, []);
useEffect(() => { useEffect(() => {
// TODO: 第一次来判断是否配置ip/DHCP、服务ip绑定终端 绑定:直接到版本更新页面 未绑定到配置ip/DHCP页面 // TODO: 第一次来判断是否配置ip/DHCP、服务ip绑定终端 绑定:直接到版本更新页面 未绑定到配置ip/DHCP页面
setTimeout(() => { // setTimeout(() => {
history.push('/login'); // history.push('/configSteps');
},9000) // },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) => { const handleMenuClick = (key: string) => {
@ -43,11 +53,11 @@ const MainLayout: React.FC = () => {
}; };
return ( return (
<Layout className="main-layout"> <div className="main-layout">
<Content className="main-content"> <div className="main-content">
<Outlet /> <Outlet />
</Content> </div>
</Layout> </div>
); );
}; };

View File

@ -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;
}
}
}
}
}

View File

@ -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;

View File

@ -0,0 +1,11 @@
import React from 'react';
const Index = () => {
return (
<div>
</div>
);
}
export default Index;

View File

@ -0,0 +1,11 @@
import React from 'react';
const Index = () => {
return (
<div>
</div>
);
}
export default Index;

View File

@ -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;
}
}

View File

@ -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;

View File

@ -1,8 +1,6 @@
.welcomeCon{ .welcomeCon{
width: 100vw; width: 100%;
height: 100vh; height: 100%;
background-color: rgba(0, 9, 51, 1);
background-size: 100% 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;

View File

@ -1,4 +1,4 @@
import axios from '@/utils/axios'; import axios from '@utils/axios';
export const BASE_URL = '/api/v1/test'; export const BASE_URL = '/api/v1/test';

View File

@ -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;
}
}

View File

@ -1,3 +1,3 @@
{ {
"extends": "./src/.umi/tsconfig.json" "extends": "./src/.umi/tsconfig.json",
} }