mirror of
https://github.com/Sagit-chu/flvx.git
synced 2026-10-02 17:06:36 +08:00
gost通讯改为ws
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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: ''
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user