Files
x-panel/web/html/servers.html
2026-05-03 11:34:48 +08:00

510 lines
23 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{{ 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" .}}