gost通讯改为ws

This commit is contained in:
qaq
2025-06-25 10:49:06 +08:00
parent f3e9b31ed3
commit f7e82cdc8b
47 changed files with 3528 additions and 1513 deletions
+63 -14
View File
@@ -197,6 +197,25 @@
</el-select>
</el-form-item>
<el-form-item label="入口端口" prop="inPort">
<el-input
v-model.number="forwardForm.inPort"
type="number"
placeholder="留空自动分配"
clearable
:min="1"
:max="65535"
>
<template slot="prepend">端口</template>
</el-input>
<div class="form-hint" v-if="selectedTunnel">
允许范围: {{ selectedTunnel.inPortSta }}-{{ selectedTunnel.inPortEnd }},留空将自动分配可用端口
</div>
<div class="form-hint" v-else>
请先选择隧道以查看端口范围,留空将自动分配可用端口
</div>
</el-form-item>
<el-form-item label="远程地址" prop="remoteAddr">
<el-input
v-model="forwardForm.remoteAddr"
@@ -264,6 +283,7 @@ export default {
userId: null,
name: '',
tunnelId: null,
inPort: null,
remoteAddr: ''
},
@@ -276,6 +296,29 @@ export default {
tunnelId: [
{ required: true, message: '请选择关联隧道', trigger: 'change' }
],
inPort: [
{
validator: (rule, value, callback) => {
if (value !== null && value !== undefined && value !== '') {
// 检查端口号范围
if (value < 1 || value > 65535) {
callback(new Error('端口号必须在1-65535之间'));
return;
}
// 检查是否在隧道允许范围内
if (this.selectedTunnel) {
if (value < this.selectedTunnel.inPortSta || value > this.selectedTunnel.inPortEnd) {
callback(new Error(`端口号必须在${this.selectedTunnel.inPortSta}-${this.selectedTunnel.inPortEnd}范围内`));
return;
}
}
}
callback();
},
trigger: 'blur'
}
],
remoteAddr: [
{ required: true, message: '请输入远程地址', trigger: 'blur' },
{
@@ -431,7 +474,8 @@ export default {
// 隧道列表加载完成后,设置表单数据并弹出对话框
this.forwardForm = {
...row,
userId: row.userId // 确保userId被正确设置
userId: row.userId, // 确保userId被正确设置
inPort: row.inPort || null // 设置入口端口
};
this.handleTunnelChange(row.tunnelId);
this.dialogVisible = true;
@@ -511,8 +555,18 @@ export default {
// 隧道选择变化处理
handleTunnelChange(tunnelId) {
this.selectedTunnel = this.tunnelList.find(tunnel =>
tunnel.id === tunnelId || tunnel.tunnelId === tunnelId
tunnel.id === tunnelId
) || null;
// 清空端口输入,避免与新隧道的端口范围冲突
this.forwardForm.inPort = null;
// 触发端口字段重新验证
this.$nextTick(() => {
if (this.$refs.forwardForm) {
this.$refs.forwardForm.clearValidate('inPort');
}
});
},
// 提交表单
@@ -530,6 +584,7 @@ export default {
userId: this.forwardForm.userId,
name: this.forwardForm.name,
tunnelId: this.forwardForm.tunnelId,
inPort: this.forwardForm.inPort || null,
remoteAddr: this.forwardForm.remoteAddr
};
res = await updateForward(updateData);
@@ -538,6 +593,7 @@ export default {
const createData = {
name: this.forwardForm.name,
tunnelId: this.forwardForm.tunnelId,
inPort: this.forwardForm.inPort || null,
remoteAddr: this.forwardForm.remoteAddr
};
res = await createForward(createData);
@@ -567,6 +623,7 @@ export default {
userId: null,
name: '',
tunnelId: null,
inPort: null,
remoteAddr: ''
};
this.selectedTunnel = null;
@@ -581,19 +638,11 @@ export default {
getTunnelDisplayName(tunnel) {
if (!tunnel) return '未知隧道';
// 处理用户隧道权限列表的数据结构
if (tunnel.tunnelId) {
const tunnelInfo = this.tunnelList.find(t => t.id === tunnel.tunnelId);
if (tunnelInfo && tunnelInfo.ip && tunnelInfo.port) {
return `${tunnelInfo.name || tunnel.tunnelId} (${tunnelInfo.ip}:${tunnelInfo.port})`;
}
return `隧道ID: ${tunnel.tunnelId}`;
}
// 处理直接隧道数据结构
// 处理隧道数据结构
if (tunnel.name) {
if (tunnel.ip && tunnel.port) {
return `${tunnel.name} (${tunnel.ip}:${tunnel.port})`;
// 显示隧道名称和IP信息
if (tunnel.ip) {
return `${tunnel.name} (${tunnel.ip})`;
}
return tunnel.name;
}
+10
View File
@@ -112,6 +112,9 @@
:width="isMobile ? '90%' : '400px'"
:before-close="handlePasswordDialogClose">
<el-form :model="passwordForm" :rules="passwordRules" ref="passwordForm" label-width="100px">
<el-form-item label="新用户名" prop="newUsername">
<el-input v-model="passwordForm.newUsername" placeholder="请输入新用户名(至少3位)" clearable></el-input>
</el-form-item>
<el-form-item label="当前密码" prop="currentPassword">
<el-input v-model="passwordForm.currentPassword" type="password" placeholder="请输入当前密码" show-password clearable></el-input>
</el-form-item>
@@ -147,11 +150,17 @@ export default {
passwordDialogVisible: false,
passwordLoading: false,
passwordForm: {
newUsername: '',
currentPassword: '',
newPassword: '',
confirmPassword: ''
},
passwordRules: {
newUsername: [
{ required: true, message: '请输入新用户名', trigger: 'blur' },
{ min: 3, message: '用户名长度至少3位', trigger: 'blur' },
{ max: 20, message: '用户名长度不能超过20位', trigger: 'blur' }
],
currentPassword: [
{ required: true, message: '请输入当前密码', trigger: 'blur' },
{ min: 1, message: '密码不能为空', trigger: 'blur' }
@@ -224,6 +233,7 @@ export default {
// 重置修改密码表单
resetPasswordForm() {
this.passwordForm = {
newUsername: '',
currentPassword: '',
newPassword: '',
confirmPassword: ''
+1 -1
View File
@@ -138,7 +138,7 @@
</div>
<!-- 空状态 -->
<div v-if="!loading && filteredSpeedLimitList.length === 0" class="empty-state" style="margin-top: 10px;">
<div v-if="!loading && filteredSpeedLimitList.length === 0" style="margin-top: 10px;">
<el-empty description="暂无限速规则">
</el-empty>
+73 -3
View File
@@ -256,6 +256,30 @@
></el-input-number>
</el-form-item>
<el-form-item label="监听地址" prop="tcpListenAddr">
<el-input
v-model="tunnelForm.tcpListenAddr"
placeholder="请输入TCP监听地址"
clearable
>
<template slot="prepend">TCP</template>
</el-input>
</el-form-item>
<el-form-item label="监听地址" prop="udpListenAddr">
<el-input
v-model="tunnelForm.udpListenAddr"
placeholder="请输入UDP监听地址"
clearable
>
<template slot="prepend">UDP</template>
</el-input>
<div class="form-hint">
部分专线需要指定才能转发udp
</div>
</el-form-item>
<!-- 只有隧道转发(type=2)时才显示出口配置 -->
<template v-if="tunnelForm.type === 2">
<el-divider content-position="left">出口配置</el-divider>
@@ -377,7 +401,9 @@ export default {
type: 1,
flow: 1, // 默认单向计算
status: 1, // 默认启用
protocol: 'tls'
protocol: 'tls',
tcpListenAddr: '0.0.0.0', // TCP监听地址
udpListenAddr: '0.0.0.0' // UDP监听地址
},
rules: {
name: [
@@ -412,6 +438,44 @@ export default {
],
protocol: [
{ required: false, message: '请选择协议类型', trigger: 'change' }
],
tcpListenAddr: [
{ required: true, message: '请输入TCP监听地址', trigger: 'blur' },
{
validator: (rule, value, callback) => {
if (value) {
// 验证IP地址格式(包括0.0.0.0)
const ipRegex = /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
if (ipRegex.test(value)) {
callback();
} else {
callback(new Error('请输入有效的IP地址'));
}
} else {
callback();
}
},
trigger: 'blur'
}
],
udpListenAddr: [
{ required: true, message: '请输入UDP监听地址', trigger: 'blur' },
{
validator: (rule, value, callback) => {
if (value) {
// 验证IP地址格式(包括0.0.0.0)
const ipRegex = /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
if (ipRegex.test(value)) {
callback();
} else {
callback(new Error('请输入有效的IP地址'));
}
} else {
callback();
}
},
trigger: 'blur'
}
]
}
};
@@ -497,6 +561,8 @@ export default {
inPortEnd: tunnel.inPortEnd,
outIpSta: tunnel.outIpSta,
outIpEnd: tunnel.outIpEnd,
tcpListenAddr: tunnel.tcpListenAddr || '0.0.0.0',
udpListenAddr: tunnel.udpListenAddr || '0.0.0.0',
// 以下字段不允许修改,但需要保留原值用于显示和提交
type: tunnel.type,
inNodeId: tunnel.inNodeId,
@@ -590,7 +656,9 @@ export default {
inPortSta: data.inPortSta,
inPortEnd: data.inPortEnd,
outIpSta: data.outIpSta,
outIpEnd: data.outIpEnd
outIpEnd: data.outIpEnd,
tcpListenAddr: data.tcpListenAddr,
udpListenAddr: data.udpListenAddr
};
updateTunnel(updateData).then(res => {
@@ -642,7 +710,9 @@ export default {
type: 1,
flow: 1, // 默认单向计算
status: 1, // 默认启用
protocol: 'tls'
protocol: 'tls',
tcpListenAddr: '0.0.0.0', // TCP监听地址
udpListenAddr: '0.0.0.0' // UDP监听地址
};
if (this.$refs.tunnelForm) {
this.$refs.tunnelForm.clearValidate();
+341 -73
View File
@@ -109,18 +109,19 @@
<div class="stat-item">
<div class="stat-header">
<i class="el-icon-position"></i>
<span>主机IP</span>
<span>服务器IP</span>
</div>
<div class="stat-content">
<el-tooltip
:content="node.connectionStatus === 'online' ? (node.ip || '-') : '-'"
placement="top"
:disabled="!node.ip || node.ip === '-'"
>
<span class="ip-value">
{{ node.connectionStatus === 'online' ? (node.ip || '-') : '-' }}
</span>
</el-tooltip>
<div v-if="node.connectionStatus === 'online' && node.serverIp" class="ip-display">
<el-tag
:type="getIpTagType(node.serverIp)"
size="mini"
class="ip-display-tag"
>
{{ node.serverIp }}
</el-tag>
</div>
<span v-else class="ip-value">-</span>
</div>
</div>
</div>
@@ -164,26 +165,9 @@
<div v-if="!loading && nodeList.length === 0" class="empty-state">
<i class="el-icon-box"></i>
<p>暂无节点数据</p>
<el-button type="primary" @click="handleAdd">创建第一个节点</el-button>
</div>
</div>
<!-- 节点与隧道转发IP重要说明 -->
<el-alert
title="重要提示"
type="warning"
:closable="false"
show-icon
style="margin-top: 30px; margin-bottom: 20px;">
<template slot="title">
<span style="font-weight: 600;">节点与隧道转发IP说明</span>
</template>
<div style="font-size: 13px; line-height: 1.5; color: #e6a23c;">
<p style="margin: 0 0 8px 0;">• 节点IP若与实际入口IP不同,可点击编辑手动更改</p>
<p style="margin: 0 0 8px 0;">• 更改节点IP后,<strong>已创建的隧道和转发IP不会自动更改</strong></p>
</div>
</el-alert>
<el-dialog
:title="dialogTitle"
:visible.sync="dialogVisible"
@@ -204,28 +188,60 @@
></el-input>
</el-form-item>
<el-form-item v-if="!isEdit" label="控制端口" prop="port">
<el-form-item label="服务器IP" prop="serverIp">
<el-input
v-model.number="nodeForm.port"
placeholder="请输入控制端口"
type="number"
min="1"
max="65535"
v-model="nodeForm.serverIp"
placeholder="请输入服务器IP地址,如: 192.168.1.100 或 2001:db8::1 或 example.com"
clearable
></el-input>
</el-form-item>
<el-form-item v-if="isEdit" label="节点IP" prop="ip">
<el-input
v-model="nodeForm.ip"
placeholder="请输入节点IP地址或域名,如: 192.168.1.1、2001:db8::1 或 example.com"
clearable
></el-input>
<el-form-item label="入口IP" prop="ipList">
<div class="ip-input-container">
<!-- 已添加的IP标签 -->
<div class="ip-tags" v-if="nodeForm.ipList.length > 0">
<el-tag
v-for="(ip, index) in nodeForm.ipList"
:key="index"
:type="getIpTagType(ip)"
size="medium"
closable
@close="removeIp(index)"
class="ip-tag"
>
{{ ip }}
</el-tag>
</div>
<!-- 输入新IP -->
<div class="ip-input-wrapper">
<el-input
v-model="newIpInput"
placeholder="请输入IP地址或域名,如: 192.168.1.1、2001:db8::1 或 example.com"
clearable
@keyup.enter.native="addIp"
@blur="addIp"
size="medium"
class="ip-input"
>
<template slot="append">
<el-button @click="addIp" icon="el-icon-plus" size="medium">添加</el-button>
</template>
</el-input>
</div>
</div>
</el-form-item>
<el-alert
v-if="!isEdit"
title="系统将自动为新节点生成UUID密钥"
title="服务器ip是用于隧道转发时,入口服务器将数据转发到此服务器"
type="info"
:closable="false"
show-icon
style="margin-bottom: 20px;">
</el-alert>
<el-alert
title="系统将自动为节点生成UUID密钥"
type="info"
:closable="false"
show-icon
@@ -280,48 +296,36 @@ export default {
nodeForm: {
id: null,
name: '',
ip: '',
port: null
ipList: [],
serverIp: ''
},
newIpInput: '',
rules: {
name: [
{ required: true, message: '请输入节点名称', trigger: 'blur' },
{ min: 2, message: '节点名称长度至少2位', trigger: 'blur' },
{ max: 50, message: '节点名称长度不能超过50位', trigger: 'blur' }
],
port: [
{ required: true, message: '请输入控制端口', trigger: 'blur' },
{ type: 'number', message: '控制端口必须为数字', trigger: 'blur' },
ipList: [
{
validator: (rule, value, callback) => {
if (value && (value < 1 || value > 65535)) {
callback(new Error('端口号必须在1-65535之间'));
if (!value || value.length === 0) {
callback(new Error('请至少添加一个入口IP地址'));
} else {
callback();
}
},
trigger: 'blur'
trigger: 'change'
}
],
ip: [
{ required: true, message: '请输入节点IP地址', trigger: 'blur' },
serverIp: [
{ required: true, message: '请输入服务器IP地址', trigger: 'blur' },
{
validator: (rule, value, callback) => {
if (value) {
// IPv4格式验证
const ipv4Regex = /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
// IPv6格式验证(完整版,支持各种压缩格式,不需要方括号)
const ipv6Regex = /^((([0-9a-fA-F]{1,4}:){7}([0-9a-fA-F]{1,4}|:))|(([0-9a-fA-F]{1,4}:){6}(:[0-9a-fA-F]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9a-fA-F]{1,4}:){5}(((:[0-9a-fA-F]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9a-fA-F]{1,4}:){4}(((:[0-9a-fA-F]{1,4}){1,3})|((:[0-9a-fA-F]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9a-fA-F]{1,4}:){3}(((:[0-9a-fA-F]{1,4}){1,4})|((:[0-9a-fA-F]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9a-fA-F]{1,4}:){2}(((:[0-9a-fA-F]{1,4}){1,5})|((:[0-9a-fA-F]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9a-fA-F]{1,4}:){1}(((:[0-9a-fA-F]{1,4}){1,6})|((:[0-9a-fA-F]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9a-fA-F]{1,4}){1,7})|((:[0-9a-fA-F]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))$/;
// 域名格式验证
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])?)*$/;
if (ipv4Regex.test(value) || ipv6Regex.test(value) || domainRegex.test(value)) {
callback();
} else {
callback(new Error('请输入有效的IP地址或域名(支持IPv4、IPv6和域名格式,如: 192.168.1.1、2001:db8::1 或 example.com)'));
}
if (!value || !value.trim()) {
callback(new Error('请输入服务器IP地址'));
} else if (!this.validateSingleIp(value.trim())) {
callback(new Error('请输入有效的IPv4、IPv6地址或域名'));
} else {
callback();
}
@@ -433,8 +437,8 @@ export default {
this.nodeForm = {
id: node.id,
name: node.name,
ip: node.ip,
port: node.port
ipList: node.ip ? (typeof node.ip === 'string' ? node.ip.split(',').filter(ip => ip.trim()) : []) : [],
serverIp: node.serverIp || ''
};
this.dialogVisible = true;
},
@@ -492,7 +496,16 @@ export default {
this.submitLoading = true;
const apiCall = this.isEdit ? updateNode : createNode;
const data = this.isEdit ? this.nodeForm : { name: this.nodeForm.name, port: this.nodeForm.port };
const submitData = {
...this.nodeForm,
ip: this.nodeForm.ipList.join(',') // 将IP数组转换为逗号分隔的字符串
};
delete submitData.ipList; // 移除ipList字段
const data = this.isEdit ? submitData : {
name: this.nodeForm.name,
ip: this.nodeForm.ipList.join(','),
serverIp: this.nodeForm.serverIp
};
apiCall(data).then(res => {
this.submitLoading = false;
@@ -505,7 +518,8 @@ export default {
const existingNode = this.nodeList.find(n => n.id === this.nodeForm.id);
if (existingNode) {
existingNode.name = this.nodeForm.name;
existingNode.ip = this.nodeForm.ip;
existingNode.ip = this.nodeForm.ipList.join(','); // 正确设置IP字段
existingNode.serverIp = this.nodeForm.serverIp; // 更新服务器IP
existingNode.updatedTime = Date.now(); // 更新时间戳
}
} else {
@@ -559,9 +573,10 @@ export default {
this.nodeForm = {
id: null,
name: '',
ip: '',
port: null
ipList: [],
serverIp: ''
};
this.newIpInput = '';
if (this.$refs.nodeForm) {
this.$refs.nodeForm.clearValidate();
}
@@ -892,6 +907,185 @@ export default {
handleResize() {
// 触发计算属性重新计算
this.$forceUpdate();
},
// 获取节点IP列表
getNodeIpList(ipString) {
if (!ipString) return [];
return ipString.split(',').map(ip => ip.trim()).filter(ip => ip);
},
// 验证IP地址格式
validateIp(ip) {
if (!ip || !ip.trim()) {
return false;
}
const trimmedIp = ip.trim();
// IPv4格式验证
const ipv4Regex = /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
// IPv6格式验证(支持各种压缩格式)
const ipv6Regex = /^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$/;
// 检查是否为IPv4
if (ipv4Regex.test(trimmedIp)) {
return true;
}
// 检查是否为IPv6
if (ipv6Regex.test(trimmedIp)) {
return true;
}
// 检查是否为特殊域名
if (trimmedIp === 'localhost') {
return true;
}
// 验证域名格式(更严格的域名验证)
// 1. 纯数字不是有效域名
if (/^\d+$/.test(trimmedIp)) {
return false;
}
// 2. 域名必须包含字母,且符合域名格式
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])?)+$/;
const singleLabelDomain = /^[a-zA-Z][a-zA-Z0-9\-]{0,62}$/; // 单标签域名,必须以字母开头
// 检查是否为有效域名(包含点号的多标签域名或单标签域名)
const isDomain = domainRegex.test(trimmedIp) || singleLabelDomain.test(trimmedIp);
if (isDomain) {
return true;
}
return false;
},
// 添加IP地址
addIp() {
if (!this.newIpInput || !this.newIpInput.trim()) return;
const ip = this.newIpInput.trim();
// 验证IP格式
if (!this.validateIp(ip)) {
this.$message({
message: '请输入有效的IP地址或域名(支持IPv4、IPv6和域名格式)',
type: 'error',
duration: 3000,
showClose: true
});
return;
}
// 检查是否已存在
if (this.nodeForm.ipList.includes(ip)) {
this.$message({
message: '该IP地址已存在',
type: 'warning',
duration: 3000,
showClose: true
});
return;
}
// 添加到列表
this.nodeForm.ipList.push(ip);
this.newIpInput = '';
// 触发表单验证
this.$nextTick(() => {
if (this.$refs.nodeForm) {
this.$refs.nodeForm.validateField('ipList');
}
});
this.$message({
message: `已添加IP地址: ${ip}`,
type: 'success',
duration: 2000
});
},
// 移除IP地址
removeIp(index) {
this.nodeForm.ipList.splice(index, 1);
// 触发表单验证
this.$nextTick(() => {
if (this.$refs.nodeForm) {
this.$refs.nodeForm.validateField('ipList');
}
});
},
// 验证单个IP地址(用于服务器IP验证)
validateSingleIp(ip) {
if (!ip || !ip.trim()) {
return false;
}
const trimmedIp = ip.trim();
// IPv4格式验证
const ipv4Regex = /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
// IPv6格式验证
const ipv6Regex = /^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$/;
// 检查是否为IPv4
if (ipv4Regex.test(trimmedIp)) {
return true;
}
// 检查是否为IPv6
if (ipv6Regex.test(trimmedIp)) {
return true;
}
// 检查是否为特殊域名
if (trimmedIp === 'localhost') {
return true;
}
// 验证域名格式(更严格的域名验证)
// 1. 纯数字不是有效域名
if (/^\d+$/.test(trimmedIp)) {
return false;
}
// 2. 域名必须包含字母,且符合域名格式
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])?)+$/;
const singleLabelDomain = /^[a-zA-Z][a-zA-Z0-9\-]{0,62}$/; // 单标签域名,必须以字母开头
// 检查是否为有效域名(包含点号的多标签域名或单标签域名)
const isDomain = domainRegex.test(trimmedIp) || singleLabelDomain.test(trimmedIp);
if (isDomain) {
return true;
}
return false;
},
// 获取IP标签类型(根据IP类型设置不同颜色)
getIpTagType(ip) {
// IPv4格式验证
const ipv4Regex = /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
// IPv6格式验证
const ipv6Regex = /^((([0-9a-fA-F]{1,4}:){7}([0-9a-fA-F]{1,4}|:))|(([0-9a-fA-F]{1,4}:){6}(:[0-9a-fA-F]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9a-fA-F]{1,4}:){5}(((:[0-9a-fA-F]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9a-fA-F]{1,4}:){4}(((:[0-9a-fA-F]{1,4}){1,3})|((:[0-9a-fA-F]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9a-fA-F]{1,4}:){3}(((:[0-9a-fA-F]{1,4}){1,4})|((:[0-9a-fA-F]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9a-fA-F]{1,4}:){2}(((:[0-9a-fA-F]{1,4}){1,5})|((:[0-9a-fA-F]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9a-fA-F]{1,4}:){1}(((:[0-9a-fA-F]{1,4}){1,6})|((:[0-9a-fA-F]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9a-fA-F]{1,4}){1,7})|((:[0-9a-fA-F]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))$/;
if (ipv4Regex.test(ip)) {
return 'primary'; // IPv4 - 蓝色
} else if (ipv6Regex.test(ip)) {
return 'success'; // IPv6 - 绿色
} else {
return 'warning'; // 域名 - 橙色
}
}
}
};
@@ -1177,6 +1371,29 @@ export default {
cursor: pointer;
}
.ip-display {
display: flex;
flex-wrap: wrap;
gap: 4px;
justify-content: center;
max-width: 100%;
overflow: hidden;
}
.ip-display-tag {
font-family: monospace;
font-size: 10px;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.ip-count-tag {
font-size: 10px;
cursor: pointer;
}
/* 空状态 */
.empty-state {
text-align: center;
@@ -1413,6 +1630,57 @@ export default {
font-weight: 500;
}
/* IP输入组件样式 */
.ip-input-container {
width: 100%;
}
.ip-tags {
margin-bottom: 12px;
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.ip-tag {
font-family: monospace;
font-size: 12px;
}
.ip-input-wrapper {
margin-bottom: 8px;
}
.ip-input {
width: 100%;
}
.ip-help-text {
font-size: 12px;
color: #909399;
line-height: 1.4;
}
.ip-help-text span {
display: block;
}
/* 响应式样式 */
@media (max-width: 768px) {
.ip-tags {
margin-bottom: 8px;
gap: 6px;
}
.ip-tag {
font-size: 11px;
}
.ip-help-text {
font-size: 11px;
}
}
/* 复制对话框样式 */
::v-deep .copy-dialog .el-message-box__content {
word-break: break-all;