Files
x-panel/web/html/servers.html

510 lines
23 KiB
HTML
Raw Normal View History

2026-05-03 11:34:48 +08:00
{{ template "page/head_start" .}}
<style>
/* 复制自 settings.html 的样式,确保布局响应式一致 */
@media (min-width: 769px) {
.ant-layout-content {
margin: 24px 16px;
}
}
/* 移动端适配 */
@media (max-width: 768px) {
.ant-layout-content {
margin: 10px 10px;
}
}
/* 调整表格卡片样式 */
.ant-card-body {
padding: 24px;
}
/* 针对暗色主题dark-theme的 a-alert 强制样式覆盖 */
/* 1、去掉暗色主题下 Modal 的默认灰色遮罩(解决“灰蒙蒙”问题) */
/*
.dark-theme .ant-modal-mask {
background: transparent !important; /* 变为完全透明,更通透 */
} */
/* 1、只去掉这个弹窗的灰色遮罩 */
.dark-theme .ping-check-modal + .ant-modal-wrap .ant-modal-mask,
.dark-theme .ping-check-modal .ant-modal-mask {
background: transparent !important;
}
/* 2、弹窗中 info 提示框 强制使用浅色背景 + 蓝色边框 */
.ant-modal .ant-alert-info {
background-color: #f0f8ff !important; /* 浅蓝色背景 */
border: 1px solid #91d5ff !important; /* 蓝色边框 */
border-radius: 8px; /* 圆角更好看 */
}
/* 3、弹窗中 a-alert 内所有文字,强制为深色(防止发灰/发白) */
.ant-modal .ant-alert-info,
.ant-modal .ant-alert-info * {
color: #333333 !important; /* 深灰文字,更清晰 */
}
/* 4、强制 info-circle 图标为标准 Ant 蓝色(恢复你之前看到的效果) */
.ant-modal .ant-alert-info .anticon-info-circle {
color: #1890ff !important;
}
/* 5、部分浏览器会把 svg 填充色变灰,这里强制修正 */
.ant-modal .ant-alert-info svg {
fill: #1890ff !important;
}
</style>
{{ template "page/head_end" .}}
{{ template "page/body_start" .}}
<a-layout id="app" v-cloak :class="themeSwitcher.currentTheme">
<a-sidebar></a-sidebar>
<a-layout id="content-layout">
<a-layout-content>
<a-spin :spinning="loading" tip="加载中...">
<transition name="list" appear>
<a-row :gutter="[isMobile ? 8 : 16, isMobile ? 0 : 12]">
<a-col>
<a-card hoverable>
<a-row>
<a-col :span="24">
<a-space :size="15">
<a-button type="primary" @click="openAddModal(0)">
<a-icon type="plus"></a-icon> 添加〔被控端 VPS
</a-button>
<a-button type="primary" style="cursor: default;">
<a-icon type="info-circle"></a-icon>
当前已绑定:[[ normalCount ]] 台 (最多可绑:[[ maxLimit ]] 台)
</a-button>
<a-button type="default" icon="reload" @click="getServers">
刷新列表
</a-button>
<a-button type="primary" icon="setting" @click="goToTgSettings">
配置本机“主控机器人”
</a-button>
</a-space>
</a-col>
</a-row>
<a-table :columns="columns" :data-source="normalServersPage" :row-key="record => record.ID" :pagination="normalPagination" @change="handleNormalTableChange" style="margin-top: 20px">
<template slot="action" slot-scope="text, record">
<a-space>
<a-button type="danger" size="small" icon="delete" @click="deleteServer(record)">删除</a-button>
</a-space>
</template>
</a-table>
</a-card>
<a-card hoverable title="一键部署中转节点">
<a-row>
<a-col :span="24">
<div style="margin-bottom: 15px; color: #666;">
<a-icon type="info-circle"></a-icon>
说明:在此添加远程中转机(落地机)信息,点击“一键部署”即可自动完成:
远程Socks创建 --> 本机路由配置 --> 本机入口创建 --> 生成“二维码和链接”。
</div>
<a-space :size="15">
<a-button type="primary" @click="openAddModal(1)">
<a-icon type="plus"></a-icon> 添加〔中转机 VPS
</a-button>
<a-button type="primary" style="cursor: default;">
<a-icon type="info-circle"></a-icon>
当前已绑定:[[ transitCount ]] 台 (最多可绑:[[ maxLimit ]] 台)
</a-button>
<a-button type="default" icon="reload" @click="getServers">
刷新列表
</a-button>
<a-button type="primary" icon="thunderbolt" @click="openCheckModal">
检测中转节点“连通性”
</a-button>
</a-space>
</a-col>
</a-row>
<a-table :columns="columns" :data-source="transitServersPage" :row-key="record => record.ID" :pagination="transitPagination" @change="handleTransitTableChange" :scroll="isMobile ? { x: 'max-content' } : {}" style="margin-top: 20px">
<template slot="action" slot-scope="text, record">
<a-space>
<a-button v-if="record.LastLink || record.last_link" type="default" size="small" icon="link" @click="getLastLink(record)" :loading="record.linkLoading">
获取上次链接
</a-button>
<a-button type="primary" size="small" icon="rocket" @click="setupRelay(record)" :loading="record.setupLoading" :type="(record.LastLink || record.last_link) ? 'dashed' : 'primary'">
[[ (record.LastLink || record.last_link) ? '重新部署' : '一键部署中转' ]]
</a-button>
<a-button type="danger" size="small" icon="delete" @click="deleteServer(record)">删除</a-button>
</a-space>
</template>
</a-table>
</a-card>
</a-col>
</a-row>
</transition>
</a-spin>
</a-layout-content>
</a-layout>
<a-modal v-model="visible" :title="formType === 1 ? '添加〔中转机 VPS' : '添加〔被控端 VPS'" @ok="submitServer" :confirm-loading="modalLoading">
<a-form :model="form" :label-col="{ span: 6 }" :wrapper-col="{ span: 16 }">
<a-form-item label="备注名称">
<a-input v-model="form.name" placeholder="例如:香港鸡"></a-input>
</a-form-item>
<a-form-item label="面板地址">
<a-input v-model="form.url" placeholder="https://aaa.xxxx.com:54321/nbxuiplus"></a-input>
<span style="font-size: 12px; color: #888;">* 必须包含协议头 (https://) 和 “端口 + 路径”</span>
</a-form-item>
<a-form-item label="用 户 名">
<a-input v-model="form.username" placeholder="面板登录用户名"></a-input>
</a-form-item>
<a-form-item label="登录密码">
<a-input-password v-model="form.password" placeholder="面板登录密码"></a-input-password>
</a-form-item>
</a-form>
<div v-if="formType === 0" style="margin-top: 20px; color: red; font-style: italic; text-align: center; line-height: 1.6;">
1、此功能用于集中管理多个X-Panel 面板〕,<br>
2、是通过在后台配置绑定机器人去管理的<br>
3、若在输入框您已经添加了被控端 的 VPS 信息〕,<br>
4、则您不能在对应的那台 VPS 中去绑定任何其他机器人,<br>
5、并且您的被控端 VPS面板不能去开启“两步验证”。
</div>
<div v-else style="margin-top: 20px; color: #1890ff; font-style: italic; text-align: center; line-height: 1.6;">
1、此功能仅用于建立VLESS Reality --> Socks5中转链路<br>
2、在此处添加的中转/落地机 VPS面板不能去开启“两步验证”<br>
3、在添加输入信息后请点击列表中的“一键部署”按钮即可去使用<br>
4、而“中转/落地机”无需做任何设置,只需确保防火墙放行端口即可,<br>
5、请勿在连“本机节点”时去进行一键部署因“Xray重启”会有卡顿<br>
6、注在已经创建好“中转节点”之后勿去随意更改“用户Email + 端口”。
</div>
</a-modal>
<a-modal v-model="resultModalVisible" :title="resultTitle" :footer="null" :width="450">
<div style="text-align: center;">
<div style="text-align: left; color: #555; white-space: pre-wrap; line-height: 1.6; margin-bottom: 20px;">[[ resultSubtitle ]]</div>
<p style="color: #888; margin-bottom: 5px;">链路:本机(Reality) --->> 中转机(Socks) --->> 互联网</p>
<div
id="qrcode-canvas"
style="display: flex; justify-content: center; margin: 10px 0; cursor: pointer;"
title="点击复制链接"
@click="copyLink"
></div>
<p style="font-size: 12px; color: #aaa;">(点击二维码或链接即可复制)</p>
<p style="font-weight:bold; margin-top:15px; text-align: left;">👇 VLESS Reality 中转链接:</p>
<a-input v-model="resultLink" read-only @click="copyLink">
<a-icon slot="addonAfter" type="copy" @click="copyLink" style="cursor: pointer;"/>
</a-input>
</div>
</a-modal>
<a-modal v-model="checkModalVisible" title="检测中转节点“连通性”" :footer="null" width="700px">
<!-- 这里是提示框:在暗黑模式下文字看不清的地方 -->
<a-alert
type="info"
show-icon
bordered
style="margin-bottom: 15px;"
>
<template slot="message">
此功能通过 TCP Ping 模拟 v2rayN 测试原理,<br><br>
检测〔上次部署生成的链接〕对应的端口是否通畅?
</template>
</a-alert>
<a-table
:columns="checkColumns"
:data-source="checkList"
:row-key="record => record.ID"
:pagination="false"
size="small"
>
<template slot="status" slot-scope="text, record">
<span v-if="record.pingLoading">
<a-icon type="loading" /> 检测中...
</span>
<span v-else>
<a-tag v-if="record.pingResult === undefined">待检测</a-tag>
<a-tag v-else-if="record.pingResult > 0" color="green">正常: [[ record.pingResult ]]ms</a-tag>
<a-tag v-else color="red">无效 / 不通</a-tag>
</span>
</template>
<template slot="action" slot-scope="text, record">
<a-button type="primary" size="small" ghost @click="performPing(record)" :loading="record.pingLoading">
立即检测
</a-button>
</template>
</a-table>
<div style="margin-top: 20px; text-align: right;">
<a-button @click="checkModalVisible = false">关闭</a-button>
</div>
</a-modal>
</a-layout>
{{template "page/body_scripts" .}}
{{template "component/aSidebar" .}}
{{template "component/aThemeSwitch" .}}
<script>
// 初始化 Vue 实例
const app = new Vue({
// 重要:修改 Vue 分隔符,防止与 Go 模板引擎冲突
delimiters: ['[[', ']]'],
// 【关键修复】:引入 MediaQueryMixin这能让布局像 settings.html 一样自动适应,消除异常间隙
mixins: [MediaQueryMixin],
el: '#app',
data: {
themeSwitcher, // 引入主题切换数据
loading: false,
modalLoading: false,
visible: false,
checkModalVisible: false,
resultModalVisible: false,
resultTitle: '部署结果',
resultSubtitle: '',
resultLink: '',
checkList: [],
checkColumns: [
{ title: 'ID', dataIndex: 'ID', key: 'ID', width: 60 },
{ title: '节点名称', dataIndex: 'name', key: 'name' },
{ title: '状态', key: 'status', scopedSlots: { customRender: 'status' } },
{ title: '操作', key: 'action', scopedSlots: { customRender: 'action' }, align: 'right' }
],
servers: [],
// 【修改这里】:定义两个独立的计数变量
normalCount: 0, // 被控端数量
transitCount: 0, // 中转机数量
maxLimit: 0,
// 【新增数据字段结束】
form: {
name: '',
url: '',
username: '',
password: '',
type: 0
},
// 用于控制模态框显示的类型状态 (0=被控端, 1=中转机)
formType: 0,
// 【分页新增】:分页配置
paginationConfig: {
pageSize: 5, // 每页显示5条
showSizeChanger: false,
hideOnSinglePage: true,
},
// 【分页新增】:当前页数
normalCurrentPage: 1,
transitCurrentPage: 1,
// 表格列定义
columns: [
{ title: 'ID', dataIndex: 'ID', key: 'ID', width: 80 },
{ title: '备注名称', dataIndex: 'name', key: 'name' },
{ title: '面板地址', dataIndex: 'url', key: 'url' },
{ title: '用户名', dataIndex: 'username', key: 'username' },
{
title: '添加时间',
dataIndex: 'CreatedAt', // 修改点1这里必须改成大写的 CreatedAt
key: 'CreatedAt',
width: 180,
// 修改点2添加格式化把 "2023-11-21T..." 变成 "2023-11-21 10:00"
customRender: (text) => {
if (!text) return '';
// 简单处理:去掉 T截取前19位
return text.replace('T', ' ').substring(0, 19);
}
},
{ title: '操作', key: 'action', scopedSlots: { customRender: 'action' }, width: 220 }
]
},
computed: {
// 过滤普通被控端 (type=0)
normalServers() {
return this.servers.filter(s => s.type === 0 || s.type === undefined);
},
// 过滤中转机 (type=1)
transitServers() {
return this.servers.filter(s => s.type === 1);
},
// 【分页核心】:普通被控端 - 当前页数据 (返回空列表)
normalServersPage() {
// 因为 this.normalServers 始终为空,所以这里返回空
return [];
},
// 【分页核心】:中转机 - 当前页数据 (返回空列表)
transitServersPage() {
// 因为 this.transitServers 始终为空,所以这里返回空
return [];
},
// 【分页核心】:分页对象 (total 始终为 0)
normalPagination() {
return {...this.paginationConfig, current: this.normalCurrentPage, total: 0};
},
// 【分页核心】:分页对象 (total 始终为 0)
transitPagination() {
return {...this.paginationConfig, current: this.transitCurrentPage, total: 0};
},
},
methods: {
// 【新增跳转逻辑】
// 点击后跳转到 settings 页面,并带上 query 参数 tab=telegram
goToTgSettings() {
window.location.href = "./settings?tab=telegram";
},
// 【分页新增】:处理普通列表分页切换
handleNormalTableChange(pagination) {
this.normalCurrentPage = pagination.current;
},
// 【分页新增】:处理中转列表分页切换
handleTransitTableChange(pagination) {
this.transitCurrentPage = pagination.current;
},
// 【拦截方法】:拦截获取链接操作
getLastLink(record) {
// 【免费版】:拦截获取链接操作
this.$message.warning("〔免费基础版〕不支持此操作");
return;
},
// 执行一键中转部署
async setupRelay(record) {
// 【免费版修改】:拦截部署操作
this.$message.warning("〔免费基础版〕不支持此操作");
return;
},
// 获取服务器列表
async getServers() {
// 1. 模拟加载结束
this.loading = false;
// 2. 强制清空列表
this.servers = [];
// 3. 【关键】:强制设置数量和额度为 0
this.normalCount = 0;
this.transitCount = 0;
this.maxLimit = 0; // 强制显示“最多可绑0 台”
},
// 打开添加弹窗
// openAddModal 方法接收 type 参数
// type: 0 = 普通被控端, 1 = 中转机
openAddModal(type = 0) {
// 1. 设置当前模态框的 UI 类型
this.formType = type;
// 2. 初始化表单,并将 type 写入表单数据中
this.form = {
name: '',
url: 'https://',
username: '',
password: '',
type: type // 【关键】:这里确保提交给后端的数据包含正确的类型
};
this.visible = true;
},
// 提交添加请求
async submitServer() {
// 1. 先关闭刚才填写的模态框,
this.visible = false;
// 2. 使用 Vue 的 createElement 函数构建支持 HTML 的提示内容
const h = this.$createElement;
// 3. 弹出警告提示框
this.$warning({
title: '功能受限提示',
okText: '知道了',
// 【核心修改点 1】调整弹窗宽度使其足够容纳一行文字
width: 580,
// 这里定义提示框的具体内容和样式
content: h('div', { style: 'margin-top: 10px; font-size: 15px; line-height: 1.6;' }, [ // 增加行高,提升阅读体验
// 第一段:设置底部外边距,模拟空行
h('p', { style: 'margin-bottom: 15px;' }, '此项功能是“付费Pro版”专属功能免费版不能用'),
// 第二段:设置底部外边距,模拟空行
h('p', { style: 'margin-bottom: 15px;' }, '请联系面板管理员〔购买授权码〕之后才能继续使用。'),
// 第三段TG 链接(无需底部外边距)
h('p', { style: 'color: #ff4d4f; font-weight: bold; margin-bottom: 0;' }, [
'----->>> “授权码购买”机器人:',
h('a', {
// 设置跳转链接到 Telegram
attrs: {
href: 'https://t.me/Buy_ShouQuan_Bot',
target: '_blank' // 在新标签页打开
},
style: {
color: '#1890ff', // 链接颜色
fontWeight: 'bold',
textDecoration: 'underline' // 链接下划线
}
}, '@Buy_ShouQuan_Bot')
]),
]),
onOk() {}
});
},
// 删除服务器
deleteServer(record) {
// 【免费版修改】:拦截删除操作
this.$message.warning("〔免费基础版〕不支持此操作");
return;
},
// 【拦截方法】:拦截【检测中转节点】操作
openCheckModal() {
// 【免费版修改】:拦截【检测中转节点】操作
this.$message.warning("〔免费基础版〕不支持此操作");
return;
},
copyLink() {
if(!this.resultLink) return;
const input = document.createElement('input');
input.value = this.resultLink;
document.body.appendChild(input);
input.select();
document.execCommand('Copy');
document.body.removeChild(input);
this.$message.success('复制成功');
},
performPing(record) {
// 占位
}
},
mounted() {
this.getServers();
}
});
</script>
{{ template "page/body_end" .}}