添加客户端控制页面,与客户端连接
This commit is contained in:
parent
691d12c5eb
commit
ce471426a1
|
|
@ -1,4 +1,5 @@
|
|||
import request from '@/utils/user_center_request';
|
||||
import local_request from '@/utils/local_request';
|
||||
|
||||
// myVPNGroup := router.Group("/vpn")
|
||||
// myVPNGroup.POST("/server_register", ServerRegisterHandler)
|
||||
|
|
@ -76,3 +77,7 @@ export const SetVPNServerConfigHandler = (Data) => {
|
|||
export const DeleteVPNServerHandler = (Data) => {
|
||||
return request.delete('/vpn/delete_vpn_server', { data: Data })
|
||||
}
|
||||
|
||||
export const LocalClientConnectHandler = (Data) => {
|
||||
return local_request.post('/vpn/connect', Data)
|
||||
}
|
||||
|
|
@ -97,6 +97,12 @@ export const menuData: Menus[] = [
|
|||
index: '/vpn-tunnel',
|
||||
title: '隧道配置',
|
||||
},
|
||||
{
|
||||
id: '754',
|
||||
pid: '75',
|
||||
index: '/vpn-client',
|
||||
title: 'VPN客户端UI',
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -155,6 +155,15 @@ const routes: RouteRecordRaw[] = [
|
|||
},
|
||||
component: () => import(/* webpackChunkName: "system-user" */ '../views/system/vpn-tunnel.vue'),
|
||||
},
|
||||
{
|
||||
path: '/vpn-client',
|
||||
name: 'vpn-client',
|
||||
meta: {
|
||||
title: 'VPN客户端UI',
|
||||
permiss: '754',
|
||||
},
|
||||
component: () => import(/* webpackChunkName: "system-user" */ '../views/system/vpn-online-connect.vue'),
|
||||
},
|
||||
{
|
||||
path: '/callback',
|
||||
name: 'callback',
|
||||
|
|
|
|||
|
|
@ -64,8 +64,9 @@ export const usePermissStore = defineStore("permiss", {
|
|||
"751", //VPN服务器配置管理
|
||||
"752", //VPN地址池管理
|
||||
"753", //VPN隧道管理
|
||||
"754", //VPN客户端UI
|
||||
],
|
||||
user: ["0", "8", "7", "9", "51" ,"53","55" ,"56", "57", "58", "59", "61", "71", "75"],
|
||||
user: ["0", "8", "7", "9", "51" ,"53","55" ,"56", "57", "58", "59", "61", "71", "75", "754"],
|
||||
},
|
||||
};
|
||||
},
|
||||
|
|
|
|||
|
|
@ -0,0 +1,89 @@
|
|||
import axios from "axios";
|
||||
import router from "@/router/index.js";
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
const baseURL= "http://localhost:18086";
|
||||
|
||||
|
||||
let isRefreshing = false;
|
||||
let requests = [];
|
||||
|
||||
const request = axios.create({
|
||||
baseURL: baseURL,
|
||||
});
|
||||
|
||||
// 请求拦截器 - 添加token
|
||||
request.interceptors.request.use(
|
||||
config => {
|
||||
const token = localStorage.getItem("token");
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
config.headers.token = token;
|
||||
}
|
||||
return config;
|
||||
}
|
||||
);
|
||||
|
||||
// 响应拦截器
|
||||
request.interceptors.response.use(
|
||||
result => {
|
||||
if(result.status !== 200) {
|
||||
router.push("/login");
|
||||
}
|
||||
|
||||
if(result.data.message === "NOT_LOGIN" || [2, 3, 4].includes(result.data.code)) {
|
||||
// 检测到token过期
|
||||
if (isRefreshing == false) {
|
||||
isRefreshing = true;
|
||||
// 这里需要替换为实际的refresh token请求
|
||||
return axios.post('https://uc.ljsea.top/user/refresh_token', {
|
||||
refresh_token: localStorage.getItem("refresh_token")
|
||||
},{
|
||||
headers: {
|
||||
'Authorization': `Bearer ${localStorage.getItem("refresh_token")}`
|
||||
}
|
||||
}).then(res => {
|
||||
const token = res.data["data"]["access_token"];
|
||||
localStorage.setItem("token", token);
|
||||
//alert("token: " + token);
|
||||
|
||||
// 重试所有挂起的请求
|
||||
requests.forEach(cb => cb(token));
|
||||
requests = [];
|
||||
isRefreshing = false;
|
||||
localStorage.setItem("refresh_time", Date.now().toString());
|
||||
|
||||
// 重试当前请求
|
||||
const config = result.config;
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
return request(config);
|
||||
}).catch(err => {
|
||||
// 刷新token失败,跳转登录
|
||||
ElMessage.error('登录已过期,请重新登录!');
|
||||
router.push("/login");
|
||||
return Promise.reject(err);
|
||||
});
|
||||
} else if (isRefreshing) {
|
||||
// 正在刷新token,将请求放入队列
|
||||
return new Promise(resolve => {
|
||||
requests.push(token => {
|
||||
resolve(request(result.config));
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if(result.data.code == 7) {
|
||||
ElMessage.error('该用户已存在,请重新输入!');
|
||||
return null;
|
||||
}
|
||||
|
||||
if(result.data.code == 1) {
|
||||
ElMessage.error('请求失败,请稍后重试!');
|
||||
} else {
|
||||
return result.data;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export default request;
|
||||
|
|
@ -0,0 +1,372 @@
|
|||
<template>
|
||||
<div class="vpn-online-container">
|
||||
<!-- 页面标题和下载客户端按钮 -->
|
||||
<div class="page-header">
|
||||
<h1>VPN在线连接</h1>
|
||||
<el-button type="primary" @click="showDownloadDialog = true">
|
||||
<el-icon><Download /></el-icon>
|
||||
下载客户端
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- VPN服务器卡片列表 -->
|
||||
<div class="vpn-cards-container" v-loading="loading">
|
||||
<el-empty v-if="!loading && vpnServers.length === 0" description="暂无在线VPN服务器" />
|
||||
|
||||
<el-row :gutter="20" v-else>
|
||||
<el-col :xs="24" :sm="12" :md="8" :lg="6" v-for="server in vpnServers" :key="server.id">
|
||||
<el-card class="vpn-server-card" shadow="hover">
|
||||
<div class="server-info">
|
||||
<div class="server-header">
|
||||
<el-icon class="server-icon"><Monitor /></el-icon>
|
||||
<h3>{{ server.name || server.server_name || 'VPN服务器' }}</h3>
|
||||
</div>
|
||||
|
||||
<div class="server-details">
|
||||
<div class="detail-item">
|
||||
<span class="label">IP地址:</span>
|
||||
<span class="value">{{ server.ip || server.server_ip || 'N/A' }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="label">端口:</span>
|
||||
<span class="value">{{ server.port || 'N/A' }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="label">状态:</span>
|
||||
<el-tag type="success" size="small">在线</el-tag>
|
||||
</div>
|
||||
<div class="detail-item" v-if="server.location">
|
||||
<span class="label">位置:</span>
|
||||
<span class="value">{{ server.location }}</span>
|
||||
</div>
|
||||
<div class="detail-item" v-if="server.protocol">
|
||||
<span class="label">协议:</span>
|
||||
<span class="value">{{ server.protocol }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-footer">
|
||||
<el-button
|
||||
type="primary"
|
||||
size="small"
|
||||
@click="connectToServer(server)"
|
||||
:loading="connectingServers.includes(server.id)"
|
||||
>
|
||||
<el-icon><Connection /></el-icon>
|
||||
连接
|
||||
</el-button>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
|
||||
<!-- 下载客户端对话框 -->
|
||||
<el-dialog
|
||||
v-model="showDownloadDialog"
|
||||
title="选择客户端下载"
|
||||
width="400px"
|
||||
:before-close="handleDialogClose"
|
||||
>
|
||||
<div class="download-options">
|
||||
<p>请选择您的操作系统类型:</p>
|
||||
<div class="client-options">
|
||||
<el-button
|
||||
class="download-btn"
|
||||
@click="downloadClient('windows')"
|
||||
:icon="Monitor"
|
||||
>
|
||||
Windows 客户端
|
||||
</el-button>
|
||||
<el-button
|
||||
class="download-btn"
|
||||
@click="downloadClient('linux')"
|
||||
:icon="Monitor"
|
||||
>
|
||||
Linux 客户端
|
||||
</el-button>
|
||||
<el-button
|
||||
class="download-btn"
|
||||
@click="downloadClient('macos')"
|
||||
:icon="Monitor"
|
||||
>
|
||||
macOS 客户端
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="showDownloadDialog = false">取消</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Download, Monitor, Connection } from '@element-plus/icons-vue'
|
||||
import { GetVPNServerOnlineListHandler, LocalClientConnectHandler } from '@/api/vpn'
|
||||
|
||||
interface VPNServer {
|
||||
id: string | number
|
||||
name?: string
|
||||
server_name?: string
|
||||
ip?: string
|
||||
server_ip?: string
|
||||
port?: string | number
|
||||
location?: string
|
||||
protocol?: string
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
const vpnServers = ref<VPNServer[]>([])
|
||||
const loading = ref(false)
|
||||
const connectingServers = ref<(string | number)[]>([])
|
||||
const showDownloadDialog = ref(false)
|
||||
|
||||
// 获取在线VPN服务器列表
|
||||
const fetchVPNServers = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const response = await GetVPNServerOnlineListHandler()
|
||||
if (response && response["code"] === 0) {
|
||||
vpnServers.value = response.data || []
|
||||
} else {
|
||||
ElMessage.error(response["message"] || '获取VPN服务器列表失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取VPN服务器列表错误:', error)
|
||||
ElMessage.error('获取VPN服务器列表失败,请稍后重试')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 连接到VPN服务器
|
||||
const connectToServer = async (server: VPNServer) => {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定要连接到服务器 ${server.name || server.server_name || server.id} 吗?`,
|
||||
'确认连接',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'info',
|
||||
}
|
||||
)
|
||||
|
||||
connectingServers.value.push(server.server_id)
|
||||
|
||||
const requestData = {
|
||||
server_id: server.server_id,
|
||||
token: localStorage.getItem('token'),
|
||||
refresh_token: localStorage.getItem('refresh_token'),
|
||||
auto_reconnect: 1,
|
||||
}
|
||||
|
||||
const response = await LocalClientConnectHandler(requestData)
|
||||
|
||||
if (response && response["code"] === 0) {
|
||||
ElMessage.success('VPN连接成功!')
|
||||
} else {
|
||||
ElMessage.error(response["message"] || 'VPN连接失败')
|
||||
}
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
console.error('VPN连接错误:', error)
|
||||
ElMessage.error('VPN连接失败,请稍后重试')
|
||||
}
|
||||
} finally {
|
||||
connectingServers.value = connectingServers.value.filter(id => id !== server.id)
|
||||
}
|
||||
}
|
||||
|
||||
// 下载客户端
|
||||
const downloadClient = (clientType: string) => {
|
||||
// 这里可以根据实际的下载链接来实现
|
||||
ElMessage.info(`正在准备${clientType}客户端下载...`)
|
||||
|
||||
// 模拟下载,实际应用中应该提供真实的下载链接
|
||||
setTimeout(() => {
|
||||
const downloadUrls = {
|
||||
windows: 'https://gitee.com/junleea/my-vpn-client/releases/download/0.1/myvpn-client-windows_amd_x64.exe',
|
||||
linux: 'https://gitee.com/junleea/my-vpn-client/releases/download/0.1/myvpn-client-windows_amd_x64.exe',
|
||||
macos: 'https://gitee.com/junleea/my-vpn-client/releases/download/0.1/myvpn-client-windows_amd_x64.exe'
|
||||
}
|
||||
|
||||
const url = downloadUrls[clientType as keyof typeof downloadUrls]
|
||||
if (url) {
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = `vpn-client-${clientType}`
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
document.body.removeChild(a)
|
||||
ElMessage.success(`${clientType}客户端下载已开始`)
|
||||
}
|
||||
|
||||
showDownloadDialog.value = false
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
// 关闭对话框
|
||||
const handleDialogClose = () => {
|
||||
showDownloadDialog.value = false
|
||||
}
|
||||
|
||||
// 页面加载时获取数据
|
||||
onMounted(() => {
|
||||
fetchVPNServers()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.vpn-online-container {
|
||||
padding: 20px;
|
||||
min-height: calc(100vh - 84px);
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.page-header h1 {
|
||||
margin: 0;
|
||||
color: #303133;
|
||||
font-size: 24px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.vpn-cards-container {
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
.vpn-server-card {
|
||||
margin-bottom: 20px;
|
||||
transition: transform 0.3s ease;
|
||||
height: 280px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.vpn-server-card:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.server-info {
|
||||
flex: 1;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.server-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
}
|
||||
|
||||
.server-icon {
|
||||
font-size: 24px;
|
||||
color: #409eff;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.server-header h3 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
color: #303133;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.server-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.label {
|
||||
color: #606266;
|
||||
min-width: 60px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.value {
|
||||
color: #303133;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
padding: 12px 16px;
|
||||
border-top: 1px solid #ebeef5;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.download-options {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.download-options p {
|
||||
margin-bottom: 20px;
|
||||
color: #606266;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.client-options {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.download-btn {
|
||||
width: 100%;
|
||||
height: 45px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.vpn-online-container {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.page-header h1 {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.vpn-server-card {
|
||||
height: 260px;
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.label {
|
||||
min-width: auto;
|
||||
margin-right: 0;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Loading…
Reference in New Issue