添加客户端控制页面,与客户端连接

This commit is contained in:
lijun 2025-12-18 20:50:31 +08:00
parent 691d12c5eb
commit ce471426a1
6 changed files with 483 additions and 1 deletions

View File

@ -1,4 +1,5 @@
import request from '@/utils/user_center_request'; import request from '@/utils/user_center_request';
import local_request from '@/utils/local_request';
// myVPNGroup := router.Group("/vpn") // myVPNGroup := router.Group("/vpn")
// myVPNGroup.POST("/server_register", ServerRegisterHandler) // myVPNGroup.POST("/server_register", ServerRegisterHandler)
@ -76,3 +77,7 @@ export const SetVPNServerConfigHandler = (Data) => {
export const DeleteVPNServerHandler = (Data) => { export const DeleteVPNServerHandler = (Data) => {
return request.delete('/vpn/delete_vpn_server', { data: Data }) return request.delete('/vpn/delete_vpn_server', { data: Data })
} }
export const LocalClientConnectHandler = (Data) => {
return local_request.post('/vpn/connect', Data)
}

View File

@ -97,6 +97,12 @@ export const menuData: Menus[] = [
index: '/vpn-tunnel', index: '/vpn-tunnel',
title: '隧道配置', title: '隧道配置',
}, },
{
id: '754',
pid: '75',
index: '/vpn-client',
title: 'VPN客户端UI',
}
], ],
}, },
{ {

View File

@ -155,6 +155,15 @@ const routes: RouteRecordRaw[] = [
}, },
component: () => import(/* webpackChunkName: "system-user" */ '../views/system/vpn-tunnel.vue'), 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', path: '/callback',
name: 'callback', name: 'callback',

View File

@ -64,8 +64,9 @@ export const usePermissStore = defineStore("permiss", {
"751", //VPN服务器配置管理 "751", //VPN服务器配置管理
"752", //VPN地址池管理 "752", //VPN地址池管理
"753", //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"],
}, },
}; };
}, },

View File

@ -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;

View File

@ -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>