mirror of
https://github.com/Sagit-chu/flvx.git
synced 2026-10-04 17:16:37 +08:00
适配h5
This commit is contained in:
+2
-1
@@ -136,5 +136,6 @@ func main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// GOOS=linux GOARCH=amd64 go build
|
// GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o gost
|
||||||
// GOOS=windows GOARCH=amd64 go build -o your_app.exe
|
// GOOS=windows GOARCH=amd64 go build -o your_app.exe
|
||||||
|
// upx --best --lzma gost
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.admin.common.utils;
|
package com.admin.common.utils;
|
||||||
|
|
||||||
|
import com.admin.common.dto.GostConfigDto;
|
||||||
import com.admin.common.dto.GostDto;
|
import com.admin.common.dto.GostDto;
|
||||||
import com.alibaba.fastjson.JSONArray;
|
import com.alibaba.fastjson.JSONArray;
|
||||||
import com.alibaba.fastjson.JSONObject;
|
import com.alibaba.fastjson.JSONObject;
|
||||||
@@ -16,10 +17,35 @@ public class GostUtil {
|
|||||||
public static GostDto SaveConfig(String addr, String secret) {
|
public static GostDto SaveConfig(String addr, String secret) {
|
||||||
JSONObject data = new JSONObject();
|
JSONObject data = new JSONObject();
|
||||||
data.put("format", "json");
|
data.put("format", "json");
|
||||||
|
|
||||||
|
if (!addr.contains("[") && addr.indexOf(':') != addr.lastIndexOf(':')) {
|
||||||
|
// 这是IPv6地址,找到最后一个冒号(端口分隔符)
|
||||||
|
int lastColonIndex = addr.lastIndexOf(':');
|
||||||
|
String ipPart = addr.substring(0, lastColonIndex);
|
||||||
|
String portPart = addr.substring(lastColonIndex);
|
||||||
|
addr = "[" + ipPart + "]" + portPart;
|
||||||
|
}
|
||||||
|
|
||||||
String url = "https://" + addr + "/api/config?format=json";
|
String url = "https://" + addr + "/api/config?format=json";
|
||||||
return HttpUtils.post(url, data, secret);
|
return HttpUtils.post(url, data, secret);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static GostConfigDto GetConfig(String addr, String secret) {
|
||||||
|
JSONObject data = new JSONObject();
|
||||||
|
data.put("format", "json");
|
||||||
|
|
||||||
|
if (!addr.contains("[") && addr.indexOf(':') != addr.lastIndexOf(':')) {
|
||||||
|
// 这是IPv6地址,找到最后一个冒号(端口分隔符)
|
||||||
|
int lastColonIndex = addr.lastIndexOf(':');
|
||||||
|
String ipPart = addr.substring(0, lastColonIndex);
|
||||||
|
String portPart = addr.substring(lastColonIndex);
|
||||||
|
addr = "[" + ipPart + "]" + portPart;
|
||||||
|
}
|
||||||
|
|
||||||
|
String url = "https://" + addr + "/api/config?format=json";
|
||||||
|
return HttpUtils.get(url, secret);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 添加限流器配置
|
* 添加限流器配置
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.admin.common.utils;
|
package com.admin.common.utils;
|
||||||
|
|
||||||
|
import com.admin.common.dto.GostConfigDto;
|
||||||
import com.admin.common.dto.GostDto;
|
import com.admin.common.dto.GostDto;
|
||||||
import com.admin.common.task.SaveConfigAsync;
|
import com.admin.common.task.SaveConfigAsync;
|
||||||
import com.admin.config.RestTemplateConfig;
|
import com.admin.config.RestTemplateConfig;
|
||||||
@@ -145,6 +146,32 @@ public class HttpUtils implements ApplicationContextAware {
|
|||||||
return restTemplate;
|
return restTemplate;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@SneakyThrows
|
||||||
|
public static GostConfigDto get(String url, String secret) {
|
||||||
|
HttpHeaders headers = new HttpHeaders();
|
||||||
|
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||||
|
headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
|
||||||
|
String auth = secret + ":" + secret;
|
||||||
|
String encodedAuth = Base64.getEncoder().encodeToString(auth.getBytes(StandardCharsets.UTF_8));
|
||||||
|
headers.set("Authorization", "Basic " + encodedAuth);
|
||||||
|
RestTemplate restTemplate = createRestTemplateWithTimeout();
|
||||||
|
HttpEntity<Object> entity = new HttpEntity<>("", headers);
|
||||||
|
try {
|
||||||
|
ResponseEntity<GostConfigDto> response = restTemplate.exchange(
|
||||||
|
url,
|
||||||
|
HttpMethod.GET,
|
||||||
|
entity,
|
||||||
|
GostConfigDto.class
|
||||||
|
);
|
||||||
|
return response.getBody();
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
GostConfigDto gostDto = new GostConfigDto();
|
||||||
|
return gostDto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@SneakyThrows
|
@SneakyThrows
|
||||||
public static GostDto post(String url, Object requestBody, String secret) {
|
public static GostDto post(String url, Object requestBody, String secret) {
|
||||||
HttpHeaders headers = new HttpHeaders();
|
HttpHeaders headers = new HttpHeaders();
|
||||||
|
|||||||
@@ -99,6 +99,7 @@ public class ForwardServiceImpl extends ServiceImpl<ForwardMapper, Forward> impl
|
|||||||
|
|
||||||
// 6. 调用Gost服务创建转发
|
// 6. 调用Gost服务创建转发
|
||||||
R gostResult = createGostServices(forward, tunnel, permissionResult.getLimiter());
|
R gostResult = createGostServices(forward, tunnel, permissionResult.getLimiter());
|
||||||
|
|
||||||
if (gostResult.getCode() != 0) {
|
if (gostResult.getCode() != 0) {
|
||||||
this.removeById(forward.getId());
|
this.removeById(forward.getId());
|
||||||
return gostResult;
|
return gostResult;
|
||||||
@@ -538,13 +539,11 @@ public class ForwardServiceImpl extends ServiceImpl<ForwardMapper, Forward> impl
|
|||||||
if (tunnel.getType() == TUNNEL_TYPE_TUNNEL_FORWARD) {
|
if (tunnel.getType() == TUNNEL_TYPE_TUNNEL_FORWARD) {
|
||||||
R chainResult = createChainService(inNode, serviceName, tunnel.getOutIp(), forward.getOutPort(), tunnel.getProtocol());
|
R chainResult = createChainService(inNode, serviceName, tunnel.getOutIp(), forward.getOutPort(), tunnel.getProtocol());
|
||||||
if (chainResult.getCode() != 0) {
|
if (chainResult.getCode() != 0) {
|
||||||
updateForwardStatusToError(forward);
|
|
||||||
return chainResult;
|
return chainResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
R remoteResult = createRemoteService(tunnel.getOutNodeId().intValue(), serviceName, forward, tunnel.getProtocol());
|
R remoteResult = createRemoteService(tunnel.getOutNodeId().intValue(), serviceName, forward, tunnel.getProtocol());
|
||||||
if (remoteResult.getCode() != 0) {
|
if (remoteResult.getCode() != 0) {
|
||||||
updateForwardStatusToError(forward);
|
|
||||||
return remoteResult;
|
return remoteResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -553,7 +552,6 @@ public class ForwardServiceImpl extends ServiceImpl<ForwardMapper, Forward> impl
|
|||||||
// 创建主服务
|
// 创建主服务
|
||||||
R serviceResult = createMainService(inNode, serviceName, forward, limiter, tunnel.getType());
|
R serviceResult = createMainService(inNode, serviceName, forward, limiter, tunnel.getType());
|
||||||
if (serviceResult.getCode() != 0) {
|
if (serviceResult.getCode() != 0) {
|
||||||
updateForwardStatusToError(forward);
|
|
||||||
return serviceResult;
|
return serviceResult;
|
||||||
}
|
}
|
||||||
return R.ok();
|
return R.ok();
|
||||||
|
|||||||
@@ -187,14 +187,12 @@ public class SpeedLimitServiceImpl extends ServiceImpl<SpeedLimitMapper, SpeedLi
|
|||||||
// 3. 获取隧道信息
|
// 3. 获取隧道信息
|
||||||
Tunnel tunnel = tunnelService.getById(speedLimit.getTunnelId());
|
Tunnel tunnel = tunnelService.getById(speedLimit.getTunnelId());
|
||||||
if (tunnel == null) {
|
if (tunnel == null) {
|
||||||
return R.err(ERROR_TUNNEL_NOT_EXISTS);
|
this.removeById(id);
|
||||||
|
return R.ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. 调用Gost API删除限速器
|
// 4. 调用Gost API删除限速器
|
||||||
R gostResult = deleteGostLimiter(id, tunnel);
|
deleteGostLimiter(id, tunnel);
|
||||||
if (gostResult.getCode() != 0) {
|
|
||||||
return gostResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5. 删除限速规则
|
// 5. 删除限速规则
|
||||||
boolean result = this.removeById(id);
|
boolean result = this.removeById(id);
|
||||||
|
|||||||
@@ -1,8 +1,30 @@
|
|||||||
package com.admin;
|
package com.admin;
|
||||||
|
|
||||||
|
import com.admin.common.dto.ConfigItem;
|
||||||
|
import com.admin.common.dto.GostConfigDto;
|
||||||
|
import com.admin.common.dto.GostDto;
|
||||||
|
import com.admin.common.utils.GostUtil;
|
||||||
|
import com.admin.entity.Forward;
|
||||||
|
import com.admin.entity.Node;
|
||||||
|
import com.admin.entity.SpeedLimit;
|
||||||
|
import com.admin.service.ForwardService;
|
||||||
|
import com.admin.service.NodeService;
|
||||||
|
import com.admin.service.SpeedLimitService;
|
||||||
|
import com.alibaba.fastjson.JSONObject;
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
import org.springframework.boot.test.context.SpringBootTest;
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
import org.xmlunit.util.Nodes;
|
||||||
|
|
||||||
|
import javax.annotation.Resource;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
@SpringBootTest
|
@SpringBootTest
|
||||||
class AdminApplicationTests {
|
class AdminApplicationTests {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,36 +1,24 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="forward-container">
|
<div class="forward-container">
|
||||||
<!-- 页面头部 -->
|
<!-- 简化的页面头部 -->
|
||||||
<div class="page-header">
|
<div class="action-buttons" style="margin-bottom: 10px;">
|
||||||
<h1 class="page-title">转发管理</h1>
|
|
||||||
<div class="header-actions">
|
|
||||||
<!-- 搜索框 -->
|
|
||||||
<el-input
|
|
||||||
v-model="searchKeyword"
|
|
||||||
placeholder="搜索转发名称或远程地址"
|
|
||||||
prefix-icon="el-icon-search"
|
|
||||||
@keyup.enter.native="handleSearch"
|
|
||||||
clearable
|
|
||||||
style="width: 250px; margin-right: 15px;"
|
|
||||||
>
|
|
||||||
<el-button slot="append" icon="el-icon-search" @click="handleSearch"></el-button>
|
|
||||||
</el-input>
|
|
||||||
|
|
||||||
<el-button
|
|
||||||
type="primary"
|
<el-button
|
||||||
icon="el-icon-plus"
|
type="primary"
|
||||||
@click="handleAdd"
|
icon="el-icon-plus"
|
||||||
class="add-btn"
|
@click="handleAdd"
|
||||||
>
|
class="add-btn"
|
||||||
新增转发
|
>
|
||||||
</el-button>
|
{{ isMobile ? '添加' : '新增转发' }}
|
||||||
</div>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 转发列表 -->
|
<!-- 转发列表 -->
|
||||||
<div class="table-container">
|
<div class="table-container">
|
||||||
<div v-loading="loading" class="cards-container">
|
<div v-loading="loading" class="cards-container">
|
||||||
<div v-if="forwardList.length === 0 && !loading" class="empty-state">
|
<!-- 空状态 -->
|
||||||
|
<div v-if="forwardList.length === 0 && !loading" class="empty-state" style="margin-top: 10px;">
|
||||||
<el-empty description="暂无转发配置"></el-empty>
|
<el-empty description="暂无转发配置"></el-empty>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -42,7 +30,7 @@
|
|||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<div class="card-title">
|
<div class="card-title">
|
||||||
<i class="el-icon-connection"></i>
|
<i class="el-icon-connection"></i>
|
||||||
{{ forward.name }}
|
<span class="title-text">{{ forward.name }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-actions">
|
<div class="card-actions">
|
||||||
<!-- 服务开关 -->
|
<!-- 服务开关 -->
|
||||||
@@ -130,7 +118,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="address-value">
|
<div class="address-value">
|
||||||
<span class="value address-text" :title="forward.remoteAddr">
|
<span class="value address-text" :title="forward.remoteAddr">
|
||||||
{{ forward.remoteAddr}}
|
{{ forward.remoteAddr }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -176,7 +164,7 @@
|
|||||||
<el-dialog
|
<el-dialog
|
||||||
:title="dialogTitle"
|
:title="dialogTitle"
|
||||||
:visible.sync="dialogVisible"
|
:visible.sync="dialogVisible"
|
||||||
width="600px"
|
:width="isMobile ? '90%' : '600px'"
|
||||||
@close="resetForm"
|
@close="resetForm"
|
||||||
>
|
>
|
||||||
<el-form
|
<el-form
|
||||||
@@ -263,6 +251,7 @@ export default {
|
|||||||
forwardList: [],
|
forwardList: [],
|
||||||
tunnelList: [],
|
tunnelList: [],
|
||||||
selectedTunnel: null,
|
selectedTunnel: null,
|
||||||
|
isMobile: false,
|
||||||
|
|
||||||
// 对话框状态
|
// 对话框状态
|
||||||
dialogVisible: false,
|
dialogVisible: false,
|
||||||
@@ -324,10 +313,24 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
created() {
|
created() {
|
||||||
|
this.checkMobile();
|
||||||
this.loadForwardList();
|
this.loadForwardList();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
window.addEventListener('resize', this.checkMobile);
|
||||||
|
},
|
||||||
|
|
||||||
|
beforeDestroy() {
|
||||||
|
window.removeEventListener('resize', this.checkMobile);
|
||||||
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
// 检查是否为移动端
|
||||||
|
checkMobile() {
|
||||||
|
this.isMobile = window.innerWidth <= 768;
|
||||||
|
},
|
||||||
|
|
||||||
// 加载转发列表
|
// 加载转发列表
|
||||||
async loadForwardList() {
|
async loadForwardList() {
|
||||||
try {
|
try {
|
||||||
@@ -721,27 +724,35 @@ export default {
|
|||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-header {
|
/* 移动端优化 */
|
||||||
display: flex;
|
@media (max-width: 768px) {
|
||||||
justify-content: space-between;
|
.forward-container {
|
||||||
align-items: center;
|
padding: 10px;
|
||||||
margin-bottom: 20px;
|
}
|
||||||
padding: 20px;
|
|
||||||
background-color: white;
|
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-title {
|
.action-buttons {
|
||||||
margin: 0;
|
|
||||||
color: #303133;
|
|
||||||
font-size: 24px;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-actions {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 移动端行为按钮样式 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.action-buttons {
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-buttons .el-input {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-btn {
|
||||||
|
flex-shrink: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.add-btn {
|
.add-btn {
|
||||||
@@ -750,7 +761,9 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.table-container {
|
.table-container {
|
||||||
padding: 20px;
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-hint {
|
.form-hint {
|
||||||
@@ -768,10 +781,6 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Element UI 样式调整 */
|
/* Element UI 样式调整 */
|
||||||
.el-table {
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-dialog {
|
.el-dialog {
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
}
|
}
|
||||||
@@ -794,6 +803,42 @@ export default {
|
|||||||
color: #606266;
|
color: #606266;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 移动端对话框优化 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.el-dialog {
|
||||||
|
border-radius: 8px;
|
||||||
|
margin: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-dialog__header {
|
||||||
|
padding: 15px 20px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-dialog__body {
|
||||||
|
padding: 10px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-dialog__footer {
|
||||||
|
padding: 10px 20px 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-dialog__title {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-form-item__label {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-footer {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-footer .el-button {
|
||||||
|
min-width: 80px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.cards-container {
|
.cards-container {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||||
@@ -801,6 +846,18 @@ export default {
|
|||||||
padding: 10px 0;
|
padding: 10px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 移动端卡片容器 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.cards-container {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 15px;
|
||||||
|
padding: 0;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.forward-card {
|
.forward-card {
|
||||||
background-color: white;
|
background-color: white;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
@@ -808,11 +865,29 @@ export default {
|
|||||||
padding: 24px;
|
padding: 24px;
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
border: 1px solid #f0f0f0;
|
border: 1px solid #f0f0f0;
|
||||||
|
overflow: hidden; /* 防止内容撑大容器 */
|
||||||
}
|
}
|
||||||
|
|
||||||
.forward-card:hover {
|
/* 移动端卡片样式 */
|
||||||
transform: translateY(-2px);
|
@media (max-width: 768px) {
|
||||||
box-shadow: 0 8px 24px rgba(0,0,0,0.15);
|
.forward-card {
|
||||||
|
padding: 16px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.forward-card:hover {
|
||||||
|
transform: none;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 桌面端悬停效果 */
|
||||||
|
@media (min-width: 769px) {
|
||||||
|
.forward-card:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 8px 24px rgba(0,0,0,0.15);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-header {
|
.card-header {
|
||||||
@@ -824,27 +899,76 @@ export default {
|
|||||||
border-bottom: 1px solid #f0f0f0;
|
border-bottom: 1px solid #f0f0f0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 移动端卡片头部 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.card-header {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.card-title {
|
.card-title {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #303133;
|
color: #303133;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 移动端卡片标题 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.card-title {
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-text {
|
||||||
|
max-width: calc(100% - 60px); /* 为操作按钮留出空间 */
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-title i {
|
.card-title i {
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
color: #409EFF;
|
color: #409EFF;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-text {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-actions {
|
.card-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 移动端卡片操作 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.card-actions {
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-actions .el-button--mini {
|
||||||
|
padding: 6px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-body {
|
.card-body {
|
||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
|
overflow: hidden; /* 防止内容撑大容器 */
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-row {
|
.info-row {
|
||||||
@@ -857,22 +981,45 @@ export default {
|
|||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 移动端信息行 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.info-row {
|
||||||
|
margin-bottom: 14px;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.info-item {
|
.info-item {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
min-width: 0; /* 确保flex子元素可以收缩 */
|
min-width: 0; /* 确保flex子元素可以收缩 */
|
||||||
|
overflow: hidden; /* 防止内容撑大容器 */
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-item.full-width {
|
.info-item.full-width {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0; /* 确保flex子元素可以收缩 */
|
min-width: 0; /* 确保flex子元素可以收缩 */
|
||||||
|
overflow: hidden; /* 防止内容撑大容器 */
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-item:not(:last-child) {
|
.info-item:not(:last-child) {
|
||||||
margin-right: 20px;
|
margin-right: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 移动端信息项 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.info-item:not(:last-child) {
|
||||||
|
margin-right: 0;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-item:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.label {
|
.label {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #909399;
|
color: #909399;
|
||||||
@@ -886,6 +1033,18 @@ export default {
|
|||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 移动端字体优化 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.label {
|
||||||
|
font-size: 11px;
|
||||||
|
margin-bottom: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.empty-state {
|
.empty-state {
|
||||||
grid-column: 1 / -1;
|
grid-column: 1 / -1;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -897,12 +1056,28 @@ export default {
|
|||||||
.flow-stats-mini {
|
.flow-stats-mini {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 移动端流量统计 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.flow-stats-mini {
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.flow-stat-item {
|
.flow-stat-item {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
min-width: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 移动端流量统计项 */
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.flow-stat-item {
|
||||||
|
min-width: 50px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.flow-stat-label {
|
.flow-stat-label {
|
||||||
@@ -918,6 +1093,18 @@ export default {
|
|||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 移动端流量统计字体 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.flow-stat-label {
|
||||||
|
font-size: 11px;
|
||||||
|
margin-bottom: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flow-stat-value {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.flow-stat-value.in {
|
.flow-stat-value.in {
|
||||||
color: #67C23A;
|
color: #67C23A;
|
||||||
}
|
}
|
||||||
@@ -963,6 +1150,7 @@ export default {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-width: 0; /* 确保flex子元素可以收缩 */
|
min-width: 0; /* 确保flex子元素可以收缩 */
|
||||||
|
overflow: hidden; /* 防止内容撑大容器 */
|
||||||
}
|
}
|
||||||
|
|
||||||
.label-with-copy {
|
.label-with-copy {
|
||||||
@@ -975,6 +1163,7 @@ export default {
|
|||||||
.address-value {
|
.address-value {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-width: 0; /* 确保flex子元素可以收缩 */
|
min-width: 0; /* 确保flex子元素可以收缩 */
|
||||||
|
overflow: hidden; /* 确保容器支持省略号 */
|
||||||
}
|
}
|
||||||
|
|
||||||
.address-text {
|
.address-text {
|
||||||
@@ -1011,6 +1200,23 @@ export default {
|
|||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 移动端复制按钮 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.copy-btn {
|
||||||
|
opacity: 0.8;
|
||||||
|
height: 18px !important;
|
||||||
|
padding: 1px 4px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-btn .el-icon-copy-document {
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.address-item:hover .copy-btn {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* 复制对话框样式 */
|
/* 复制对话框样式 */
|
||||||
::v-deep .copy-dialog .el-message-box__content {
|
::v-deep .copy-dialog .el-message-box__content {
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
@@ -1032,5 +1238,75 @@ export default {
|
|||||||
-webkit-user-select: text;
|
-webkit-user-select: text;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 移动端全局优化 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
/* 确保内容不会水平滚动 */
|
||||||
|
.forward-container * {
|
||||||
|
box-sizing: border-box;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 强制约束容器宽度 */
|
||||||
|
.forward-card,
|
||||||
|
.card-body,
|
||||||
|
.info-row,
|
||||||
|
.info-item,
|
||||||
|
.address-item,
|
||||||
|
.address-value {
|
||||||
|
max-width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 优化长文本显示 - 保持省略号不换行 */
|
||||||
|
.address-text {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 标题文本约束 */
|
||||||
|
.title-text {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 移动端空状态调整 */
|
||||||
|
.empty-state {
|
||||||
|
padding: 40px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 移动端form hint */
|
||||||
|
.form-hint {
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 超小屏幕优化 */
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.forward-container {
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-buttons {
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.forward-card {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-row {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
+152
-21
@@ -1,7 +1,16 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-container>
|
<el-container>
|
||||||
<el-header style="height: 50px;">
|
<el-header style="height: 50px;">
|
||||||
<span style="line-height: 50px">管理后台</span>
|
<div class="header-left">
|
||||||
|
<!-- 移动端菜单按钮 -->
|
||||||
|
<el-button
|
||||||
|
icon="el-icon-menu"
|
||||||
|
class="mobile-menu-btn"
|
||||||
|
@click="toggleMobileMenu"
|
||||||
|
type="text">
|
||||||
|
</el-button>
|
||||||
|
<span style="line-height: 50px">管理后台</span>
|
||||||
|
</div>
|
||||||
<div class="header-avatar">
|
<div class="header-avatar">
|
||||||
<el-avatar style="margin-right: 10px" size="medium" src="https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png"></el-avatar>
|
<el-avatar style="margin-right: 10px" size="medium" src="https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png"></el-avatar>
|
||||||
<el-dropdown>
|
<el-dropdown>
|
||||||
@@ -22,13 +31,19 @@
|
|||||||
</el-header>
|
</el-header>
|
||||||
|
|
||||||
<el-container>
|
<el-container>
|
||||||
<el-aside width="200px">
|
<!-- 移动端遮罩层 -->
|
||||||
|
<div v-if="mobileMenuVisible" class="mobile-overlay" @click="hideMobileMenu"></div>
|
||||||
|
|
||||||
|
<el-aside
|
||||||
|
:width="isMobile ? '250px' : '200px'"
|
||||||
|
:class="['main-aside', { 'mobile-aside': isMobile, 'mobile-hidden': isMobile && !mobileMenuVisible }]">
|
||||||
<el-menu
|
<el-menu
|
||||||
:default-active="path"
|
:default-active="path"
|
||||||
class="el-menu-vertical-demo">
|
class="el-menu-vertical-demo"
|
||||||
|
:collapse="false">
|
||||||
|
|
||||||
<router-link to="/index">
|
<router-link to="/index">
|
||||||
<el-menu-item index="/index" @click="save('/index')">
|
<el-menu-item index="/index" @click="handleMenuClick('/index')">
|
||||||
<template slot="title">
|
<template slot="title">
|
||||||
<i class="el-icon-box"></i>
|
<i class="el-icon-box"></i>
|
||||||
<span slot="title">首页</span>
|
<span slot="title">首页</span>
|
||||||
@@ -37,7 +52,7 @@
|
|||||||
</router-link>
|
</router-link>
|
||||||
|
|
||||||
<router-link to="/limit" v-admin>
|
<router-link to="/limit" v-admin>
|
||||||
<el-menu-item index="/limit" @click="save('/limit')">
|
<el-menu-item index="/limit" @click="handleMenuClick('/limit')">
|
||||||
<template slot="title">
|
<template slot="title">
|
||||||
<i class="el-icon-box"></i>
|
<i class="el-icon-box"></i>
|
||||||
<span slot="title">限速管理</span>
|
<span slot="title">限速管理</span>
|
||||||
@@ -46,7 +61,7 @@
|
|||||||
</router-link>
|
</router-link>
|
||||||
|
|
||||||
<router-link to="/forward">
|
<router-link to="/forward">
|
||||||
<el-menu-item index="/forward" @click="save('/forward')">
|
<el-menu-item index="/forward" @click="handleMenuClick('/forward')">
|
||||||
<template slot="title">
|
<template slot="title">
|
||||||
<i class="el-icon-postcard"></i>
|
<i class="el-icon-postcard"></i>
|
||||||
<span slot="title">转发管理</span>
|
<span slot="title">转发管理</span>
|
||||||
@@ -55,7 +70,7 @@
|
|||||||
</router-link>
|
</router-link>
|
||||||
|
|
||||||
<router-link to="/tunnel" v-admin>
|
<router-link to="/tunnel" v-admin>
|
||||||
<el-menu-item index="/tunnel" @click="save('/tunnel')">
|
<el-menu-item index="/tunnel" @click="handleMenuClick('/tunnel')">
|
||||||
<template slot="title">
|
<template slot="title">
|
||||||
<i class="el-icon-postcard"></i>
|
<i class="el-icon-postcard"></i>
|
||||||
<span slot="title">隧道管理</span>
|
<span slot="title">隧道管理</span>
|
||||||
@@ -64,7 +79,7 @@
|
|||||||
</router-link>
|
</router-link>
|
||||||
|
|
||||||
<router-link to="/node" v-admin>
|
<router-link to="/node" v-admin>
|
||||||
<el-menu-item index="/node" @click="save('/node')">
|
<el-menu-item index="/node" @click="handleMenuClick('/node')">
|
||||||
<template slot="title">
|
<template slot="title">
|
||||||
<i class="el-icon-postcard"></i>
|
<i class="el-icon-postcard"></i>
|
||||||
<span slot="title">节点监控</span>
|
<span slot="title">节点监控</span>
|
||||||
@@ -73,7 +88,7 @@
|
|||||||
</router-link>
|
</router-link>
|
||||||
|
|
||||||
<router-link to="/user" v-admin>
|
<router-link to="/user" v-admin>
|
||||||
<el-menu-item index="/user" @click="save('/user')">
|
<el-menu-item index="/user" @click="handleMenuClick('/user')">
|
||||||
<template slot="title">
|
<template slot="title">
|
||||||
<i class="el-icon-user"></i>
|
<i class="el-icon-user"></i>
|
||||||
<span slot="title">用户管理</span>
|
<span slot="title">用户管理</span>
|
||||||
@@ -83,11 +98,11 @@
|
|||||||
|
|
||||||
</el-menu>
|
</el-menu>
|
||||||
</el-aside>
|
</el-aside>
|
||||||
<el-main>
|
<el-main :class="['main-content', { 'mobile-main': isMobile }]">
|
||||||
<div style="margin: 15px 15px;">
|
<div :style="mainContentStyle">
|
||||||
<router-view/>
|
<router-view/>
|
||||||
|
|
||||||
<div style="background-color: white; height: 50px; position: absolute; bottom: 0;left: 199px;right: 0;line-height: 50px; padding-left: 30px;font-size: 12px;color:#58666e;">
|
<div :class="['footer', { 'mobile-footer': isMobile }]">
|
||||||
© 2023 Copyright.
|
© 2023 Copyright.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -95,7 +110,11 @@
|
|||||||
</el-container>
|
</el-container>
|
||||||
|
|
||||||
<!-- 修改密码弹窗 -->
|
<!-- 修改密码弹窗 -->
|
||||||
<el-dialog title="修改密码" :visible.sync="passwordDialogVisible" width="400px" :before-close="handlePasswordDialogClose">
|
<el-dialog
|
||||||
|
title="修改密码"
|
||||||
|
:visible.sync="passwordDialogVisible"
|
||||||
|
:width="isMobile ? '90%' : '400px'"
|
||||||
|
:before-close="handlePasswordDialogClose">
|
||||||
<el-form :model="passwordForm" :rules="passwordRules" ref="passwordForm" label-width="100px">
|
<el-form :model="passwordForm" :rules="passwordRules" ref="passwordForm" label-width="100px">
|
||||||
<el-form-item label="当前密码" prop="currentPassword">
|
<el-form-item label="当前密码" prop="currentPassword">
|
||||||
<el-input v-model="passwordForm.currentPassword" type="password" placeholder="请输入当前密码" show-password clearable></el-input>
|
<el-input v-model="passwordForm.currentPassword" type="password" placeholder="请输入当前密码" show-password clearable></el-input>
|
||||||
@@ -126,6 +145,8 @@ export default {
|
|||||||
return{
|
return{
|
||||||
path:'',
|
path:'',
|
||||||
name: '',
|
name: '',
|
||||||
|
isMobile: false,
|
||||||
|
mobileMenuVisible: false,
|
||||||
// 修改密码相关数据
|
// 修改密码相关数据
|
||||||
passwordDialogVisible: false,
|
passwordDialogVisible: false,
|
||||||
passwordLoading: false,
|
passwordLoading: false,
|
||||||
@@ -151,11 +172,42 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
mainContentStyle() {
|
||||||
|
return {
|
||||||
|
margin: this.isMobile ? '10px' : '15px 15px'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
created() {
|
created() {
|
||||||
this.path = localStorage.getItem('e')
|
this.path = localStorage.getItem('e')
|
||||||
this.name = localStorage.getItem("name")
|
this.name = localStorage.getItem("name")
|
||||||
|
this.checkMobile()
|
||||||
|
window.addEventListener('resize', this.checkMobile)
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
window.removeEventListener('resize', this.checkMobile)
|
||||||
},
|
},
|
||||||
methods:{
|
methods:{
|
||||||
|
checkMobile() {
|
||||||
|
this.isMobile = window.innerWidth <= 768
|
||||||
|
if (!this.isMobile) {
|
||||||
|
this.mobileMenuVisible = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
toggleMobileMenu() {
|
||||||
|
this.mobileMenuVisible = !this.mobileMenuVisible
|
||||||
|
},
|
||||||
|
hideMobileMenu() {
|
||||||
|
this.mobileMenuVisible = false
|
||||||
|
},
|
||||||
|
handleMenuClick(path) {
|
||||||
|
this.save(path)
|
||||||
|
// 移动端点击菜单后隐藏菜单
|
||||||
|
if (this.isMobile) {
|
||||||
|
this.hideMobileMenu()
|
||||||
|
}
|
||||||
|
},
|
||||||
save(e){
|
save(e){
|
||||||
localStorage.setItem('e',e)
|
localStorage.setItem('e',e)
|
||||||
this.path = e
|
this.path = e
|
||||||
@@ -242,8 +294,29 @@ export default {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.el-header {
|
||||||
|
background-color: #2F54EB;
|
||||||
|
color: white;
|
||||||
|
line-height: 50px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-menu-btn {
|
||||||
|
display: none;
|
||||||
|
color: white;
|
||||||
|
font-size: 18px;
|
||||||
|
margin-right: 10px;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.header-avatar {
|
.header-avatar {
|
||||||
float: right;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-around;
|
justify-content: space-around;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -253,25 +326,68 @@ export default {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-header {
|
.main-aside {
|
||||||
background-color: #2F54EB;
|
|
||||||
color: white;
|
color: white;
|
||||||
line-height: 50px;
|
transition: transform 0.3s ease;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1000;
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-aside {
|
.mobile-aside {
|
||||||
color: white;
|
position: fixed;
|
||||||
line-height: 200px;
|
top: 50px;
|
||||||
|
left: 0;
|
||||||
|
height: calc(100vh - 50px);
|
||||||
|
z-index: 1001;
|
||||||
|
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.15);
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-main {
|
.mobile-hidden {
|
||||||
|
transform: translateX(-100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 50px;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
height: calc(100vh - 100px);
|
height: calc(100vh - 100px);
|
||||||
|
transition: margin-left 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-main {
|
||||||
|
margin-left: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
background-color: white;
|
||||||
|
height: 50px;
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 199px;
|
||||||
|
right: 0;
|
||||||
|
line-height: 50px;
|
||||||
|
padding-left: 30px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #58666e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-footer {
|
||||||
|
left: 0;
|
||||||
|
padding-left: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-menu-vertical-demo {
|
.el-menu-vertical-demo {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
@@ -288,4 +404,19 @@ a {
|
|||||||
.el-form-item {
|
.el-form-item {
|
||||||
margin-bottom: 22px;
|
margin-bottom: 22px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 移动端适配 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.mobile-menu-btn {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-avatar .el-avatar {
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-dropdown-link {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -838,6 +838,7 @@ export default {
|
|||||||
white-space: nowrap; /* 不换行 */
|
white-space: nowrap; /* 不换行 */
|
||||||
overflow: hidden; /* 超出隐藏 */
|
overflow: hidden; /* 超出隐藏 */
|
||||||
text-overflow: ellipsis; /* 超出显示省略号 */
|
text-overflow: ellipsis; /* 超出显示省略号 */
|
||||||
|
word-break: break-all; /* 移动端强制换行 */
|
||||||
}
|
}
|
||||||
|
|
||||||
.address-source {
|
.address-source {
|
||||||
@@ -912,44 +913,267 @@ export default {
|
|||||||
/* 响应式设计 */
|
/* 响应式设计 */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.package-container {
|
.package-container {
|
||||||
padding: 10px;
|
padding: 8px;
|
||||||
|
background-color: #f5f5f5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-header {
|
.overview-card {
|
||||||
|
padding: 16px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 15px;
|
align-items: flex-start;
|
||||||
align-items: stretch;
|
gap: 8px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header h3 {
|
||||||
|
font-size: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.overview-stats {
|
.overview-stats {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-item {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-icon {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
margin-right: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-icon i {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.progress-section {
|
.progress-section {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-item {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 16px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
padding: 0 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tunnels-grid {
|
.tunnels-grid {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tunnel-card {
|
||||||
|
padding: 16px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tunnel-header {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tunnel-name {
|
||||||
|
font-size: 15px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tunnel-status {
|
||||||
|
justify-content: flex-start;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tunnel-stat-row {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tunnel-stat-item {
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 6px 8px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tunnel-stat-item:first-child {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-mini-header {
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tunnel-footer {
|
||||||
|
margin-top: 12px;
|
||||||
|
padding-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reset-time {
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.forwards-container {
|
||||||
|
padding: 16px;
|
||||||
|
margin: 0 -4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.forward-group {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-header {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
padding-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-header h4 {
|
||||||
|
font-size: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.forward-list {
|
.forward-list {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
|
||||||
|
|
||||||
.forward-item {
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.forward-info {
|
.forward-item {
|
||||||
width: 100%;
|
padding: 12px;
|
||||||
margin-bottom: 8px;
|
border-left-width: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.forward-item:hover {
|
||||||
|
transform: none;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.forward-name {
|
||||||
|
font-size: 15px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.forward-address {
|
||||||
|
gap: 2px;
|
||||||
|
padding: 6px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.address-source,
|
||||||
|
.address-target {
|
||||||
|
font-size: 13px;
|
||||||
|
padding: 3px 8px;
|
||||||
|
min-width: 100px;
|
||||||
|
max-width: 100%;
|
||||||
|
white-space: normal; /* 移动端允许换行 */
|
||||||
|
word-break: break-all; /* 强制换行 */
|
||||||
|
text-overflow: inherit; /* 取消省略号 */
|
||||||
|
overflow: visible; /* 显示完整内容 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.address-arrow {
|
||||||
|
font-size: 16px;
|
||||||
|
margin: 1px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.forward-traffic {
|
.forward-traffic {
|
||||||
width: 100%;
|
padding: 6px;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
.traffic-item {
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.traffic-label {
|
||||||
|
font-size: 11px;
|
||||||
|
margin-bottom: 0;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.traffic-value {
|
||||||
|
font-size: 13px;
|
||||||
|
padding: 1px 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
height: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state .el-empty {
|
||||||
|
padding: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 移动端进度条调整 */
|
||||||
|
.el-progress {
|
||||||
|
margin: 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-progress-bar__outer {
|
||||||
|
height: 6px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-progress-bar__inner {
|
||||||
|
height: 6px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 移动端标签样式优化 */
|
||||||
|
.el-tag {
|
||||||
|
font-size: 11px !important;
|
||||||
|
padding: 0 6px !important;
|
||||||
|
height: 20px !important;
|
||||||
|
line-height: 18px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 移动端卡片阴影减少 */
|
||||||
|
.overview-card,
|
||||||
|
.tunnel-card,
|
||||||
|
.forwards-container {
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 移动端触摸友好的按钮间距 */
|
||||||
|
.tunnel-status .el-tag {
|
||||||
|
margin: 2px 4px 2px 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 移动端图标调整 */
|
||||||
|
.tunnel-name i,
|
||||||
|
.reset-time i {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 避免移动端横向滚动 */
|
||||||
|
.package-container,
|
||||||
|
.overview-card,
|
||||||
|
.tunnel-card,
|
||||||
|
.forwards-container,
|
||||||
|
.forward-item {
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
+389
-136
@@ -1,116 +1,154 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="speed-limit-container">
|
<div class="speed-limit-container">
|
||||||
<!-- 页面头部 -->
|
<div class="action-buttons" style="margin-bottom: 10px;">
|
||||||
<div class="page-header">
|
<el-button
|
||||||
<h1 class="page-title">
|
type="primary"
|
||||||
<i class="el-icon-odometer"></i>
|
icon="el-icon-plus"
|
||||||
限速管理
|
@click="handleAdd"
|
||||||
</h1>
|
class="add-btn"
|
||||||
<div class="header-actions">
|
>
|
||||||
<!-- 搜索框 -->
|
新增限速
|
||||||
<el-input
|
</el-button>
|
||||||
v-model="searchKeyword"
|
</div>
|
||||||
placeholder="搜索规则名称"
|
|
||||||
prefix-icon="el-icon-search"
|
|
||||||
@keyup.enter.native="handleSearch"
|
|
||||||
clearable
|
|
||||||
style="width: 250px; margin-right: 15px;"
|
|
||||||
>
|
|
||||||
<el-button slot="append" icon="el-icon-search" @click="handleSearch"></el-button>
|
|
||||||
</el-input>
|
|
||||||
|
|
||||||
<el-button
|
<!-- 加载状态 -->
|
||||||
type="primary"
|
<div v-if="loading" class="loading-state">
|
||||||
icon="el-icon-plus"
|
<div class="loading-spinner"></div>
|
||||||
@click="handleAdd"
|
<p>加载中...</p>
|
||||||
class="add-btn"
|
</div>
|
||||||
>
|
|
||||||
新增限速规则
|
|
||||||
</el-button>
|
|
||||||
|
|
||||||
<el-button
|
<!-- 限速规则列表 - 桌面端表格 -->
|
||||||
type="success"
|
<div v-else-if="!isMobile && !loading && filteredSpeedLimitList.length !== 0" class="table-container">
|
||||||
icon="el-icon-refresh"
|
<div class="custom-table">
|
||||||
@click="loadSpeedLimits"
|
<div class="table-header">
|
||||||
:loading="loading"
|
<div class="th">ID</div>
|
||||||
>
|
<div class="th">规则名称</div>
|
||||||
刷新
|
<div class="th">速度限制</div>
|
||||||
</el-button>
|
<div class="th">状态</div>
|
||||||
|
<div class="th">绑定隧道</div>
|
||||||
|
<div class="th">创建时间</div>
|
||||||
|
<div class="th">更新时间</div>
|
||||||
|
<div class="th">操作</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-for="rule in filteredSpeedLimitList" :key="rule.id" class="table-row">
|
||||||
|
<div class="td">{{ rule.id }}</div>
|
||||||
|
<div class="td">{{ rule.name }}</div>
|
||||||
|
<div class="td">
|
||||||
|
<el-tag type="info">{{ rule.speed }} Mbps</el-tag>
|
||||||
|
</div>
|
||||||
|
<div class="td">
|
||||||
|
<el-tag :type="rule.status === 1 ? 'success' : 'danger'">
|
||||||
|
{{ rule.status === 1 ? '运行' : '异常' }}
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
<div class="td">
|
||||||
|
<el-tag v-if="rule.tunnelName" type="success">{{ rule.tunnelName }}</el-tag>
|
||||||
|
<span v-else class="text-muted">未绑定</span>
|
||||||
|
</div>
|
||||||
|
<div class="td">{{ rule.createdTime | dateFormat }}</div>
|
||||||
|
<div class="td">{{ rule.updatedTime | dateFormat }}</div>
|
||||||
|
<div class="td">
|
||||||
|
<div class="action-buttons-inline">
|
||||||
|
<el-button
|
||||||
|
size="mini"
|
||||||
|
type="primary"
|
||||||
|
icon="el-icon-edit"
|
||||||
|
@click="handleEdit(rule)"
|
||||||
|
circle
|
||||||
|
></el-button>
|
||||||
|
<el-button
|
||||||
|
size="mini"
|
||||||
|
type="danger"
|
||||||
|
icon="el-icon-delete"
|
||||||
|
@click="handleDelete(rule)"
|
||||||
|
circle
|
||||||
|
></el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 限速规则列表 -->
|
<!-- 限速规则列表 - 移动端卡片 -->
|
||||||
<div class="table-container">
|
<div v-else class="cards-container" v-loading="loading">
|
||||||
<el-table
|
<div
|
||||||
:data="speedLimitList"
|
v-for="rule in filteredSpeedLimitList"
|
||||||
v-loading="loading"
|
:key="rule.id"
|
||||||
stripe
|
class="rule-card"
|
||||||
border
|
|
||||||
style="width: 100%"
|
|
||||||
>
|
>
|
||||||
<el-table-column prop="id" label="ID" width="80"></el-table-column>
|
<div class="card-header">
|
||||||
<el-table-column prop="name" label="规则名称" show-overflow-tooltip></el-table-column>
|
<div class="card-title">
|
||||||
<el-table-column prop="speed" label="速度限制" >
|
<i class="el-icon-odometer"></i>
|
||||||
<template slot-scope="scope">
|
{{ rule.name }}
|
||||||
<el-tag type="info">{{ scope.row.speed }} Mbps</el-tag>
|
</div>
|
||||||
</template>
|
<div class="card-status">
|
||||||
</el-table-column>
|
<el-tag :type="rule.status === 1 ? 'success' : 'danger'" size="mini">
|
||||||
<el-table-column prop="status" label="状态" width="100">
|
{{ rule.status === 1 ? '运行' : '异常' }}
|
||||||
<template slot-scope="scope">
|
|
||||||
<el-tag :type="scope.row.status === 1 ? 'success' : 'danger'">
|
|
||||||
{{ scope.row.status === 1 ? '运行' : '异常' }}
|
|
||||||
</el-tag>
|
</el-tag>
|
||||||
</template>
|
</div>
|
||||||
</el-table-column>
|
</div>
|
||||||
<el-table-column prop="tunnelName" label="绑定隧道" show-overflow-tooltip>
|
|
||||||
<template slot-scope="scope">
|
|
||||||
<el-tag type="success" v-if="scope.row.tunnelName">{{ scope.row.tunnelName }}</el-tag>
|
|
||||||
<span v-else class="text-muted">未绑定</span>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column prop="createdTime" label="创建时间" >
|
|
||||||
<template slot-scope="scope">
|
|
||||||
{{ scope.row.createdTime | dateFormat }}
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column prop="updatedTime" label="更新时间" >
|
|
||||||
<template slot-scope="scope">
|
|
||||||
{{ scope.row.updatedTime | dateFormat }}
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="操作" fixed="right">
|
|
||||||
<template slot-scope="scope">
|
|
||||||
<el-button
|
|
||||||
size="mini"
|
|
||||||
type="primary"
|
|
||||||
icon="el-icon-edit"
|
|
||||||
@click="handleEdit(scope.row)"
|
|
||||||
>
|
|
||||||
</el-button>
|
|
||||||
<el-button
|
|
||||||
size="mini"
|
|
||||||
type="danger"
|
|
||||||
icon="el-icon-delete"
|
|
||||||
@click="handleDelete(scope.row)"
|
|
||||||
>
|
|
||||||
</el-button>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
</el-table>
|
|
||||||
|
|
||||||
<!-- 空状态 -->
|
<div class="card-content">
|
||||||
<div v-if="!loading && speedLimitList.length === 0" class="empty-state">
|
<div class="info-row">
|
||||||
<el-empty description="暂无限速规则">
|
<span class="info-label">ID:</span>
|
||||||
<el-button type="primary" @click="handleAdd">创建第一个限速规则</el-button>
|
<span class="info-value">{{ rule.id }}</span>
|
||||||
</el-empty>
|
</div>
|
||||||
|
<div class="info-row">
|
||||||
|
<span class="info-label">速度限制:</span>
|
||||||
|
<span class="info-value speed-value">{{ rule.speed }} Mbps</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-row">
|
||||||
|
<span class="info-label">绑定隧道:</span>
|
||||||
|
<span class="info-value tunnel-value">
|
||||||
|
{{ rule.tunnelName || '未绑定' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-row">
|
||||||
|
<span class="info-label">创建时间:</span>
|
||||||
|
<span class="info-value">{{ rule.createdTime | dateFormat }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-row">
|
||||||
|
<span class="info-label">更新时间:</span>
|
||||||
|
<span class="info-value">{{ rule.updatedTime | dateFormat }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-actions">
|
||||||
|
<el-button
|
||||||
|
@click="handleEdit(rule)"
|
||||||
|
type="primary"
|
||||||
|
size="small"
|
||||||
|
icon="el-icon-edit"
|
||||||
|
plain
|
||||||
|
>
|
||||||
|
编辑
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
@click="handleDelete(rule)"
|
||||||
|
type="danger"
|
||||||
|
size="small"
|
||||||
|
icon="el-icon-delete"
|
||||||
|
plain
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 空状态 -->
|
||||||
|
<div v-if="!loading && filteredSpeedLimitList.length === 0" class="empty-state" style="margin-top: 10px;">
|
||||||
|
<el-empty description="暂无限速规则">
|
||||||
|
|
||||||
|
</el-empty>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 新增/编辑对话框 -->
|
<!-- 新增/编辑对话框 -->
|
||||||
<el-dialog
|
<el-dialog
|
||||||
:title="dialogTitle"
|
:title="dialogTitle"
|
||||||
:visible.sync="dialogVisible"
|
:visible.sync="dialogVisible"
|
||||||
width="500px"
|
:width="isMobile ? '90%' : '500px'"
|
||||||
@close="resetForm"
|
@close="resetForm"
|
||||||
>
|
>
|
||||||
<el-form
|
<el-form
|
||||||
@@ -136,12 +174,10 @@
|
|||||||
style="width: 100%;"
|
style="width: 100%;"
|
||||||
></el-input-number>
|
></el-input-number>
|
||||||
<div class="form-hint">
|
<div class="form-hint">
|
||||||
单位:Mbps
|
单位:Mbps,范围:1-1000000
|
||||||
</div>
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<el-form-item label="绑定隧道" prop="tunnelId">
|
<el-form-item label="绑定隧道" prop="tunnelId">
|
||||||
<el-select
|
<el-select
|
||||||
v-model="speedLimitForm.tunnelId"
|
v-model="speedLimitForm.tunnelId"
|
||||||
@@ -192,6 +228,7 @@ export default {
|
|||||||
isEdit: false,
|
isEdit: false,
|
||||||
submitLoading: false,
|
submitLoading: false,
|
||||||
searchKeyword: '',
|
searchKeyword: '',
|
||||||
|
isMobile: false,
|
||||||
|
|
||||||
// 表单数据
|
// 表单数据
|
||||||
speedLimitForm: {
|
speedLimitForm: {
|
||||||
@@ -220,12 +257,33 @@ export default {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
filteredSpeedLimitList() {
|
||||||
|
if (!this.searchKeyword.trim()) {
|
||||||
|
return this.speedLimitList;
|
||||||
|
}
|
||||||
|
return this.speedLimitList.filter(item =>
|
||||||
|
item.name.toLowerCase().includes(this.searchKeyword.toLowerCase())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
|
this.checkMobile();
|
||||||
this.loadSpeedLimits();
|
this.loadSpeedLimits();
|
||||||
this.loadTunnels();
|
this.loadTunnels();
|
||||||
|
window.addEventListener('resize', this.checkMobile);
|
||||||
|
},
|
||||||
|
|
||||||
|
beforeDestroy() {
|
||||||
|
window.removeEventListener('resize', this.checkMobile);
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
checkMobile() {
|
||||||
|
this.isMobile = window.innerWidth <= 768;
|
||||||
|
},
|
||||||
|
|
||||||
// 加载限速规则列表
|
// 加载限速规则列表
|
||||||
async loadSpeedLimits() {
|
async loadSpeedLimits() {
|
||||||
try {
|
try {
|
||||||
@@ -260,20 +318,7 @@ export default {
|
|||||||
|
|
||||||
// 搜索功能
|
// 搜索功能
|
||||||
handleSearch() {
|
handleSearch() {
|
||||||
if (!this.searchKeyword.trim()) {
|
// 通过 computed 属性实现实时搜索
|
||||||
this.loadSpeedLimits();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 实现本地搜索过滤
|
|
||||||
this.loading = true;
|
|
||||||
setTimeout(() => {
|
|
||||||
const filtered = this.speedLimitList.filter(item =>
|
|
||||||
item.name.includes(this.searchKeyword)
|
|
||||||
);
|
|
||||||
this.speedLimitList = filtered;
|
|
||||||
this.loading = false;
|
|
||||||
}, 300);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// 处理隧道选择变化
|
// 处理隧道选择变化
|
||||||
@@ -400,14 +445,15 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.page-header {
|
.page-header {
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
background-color: white;
|
background-color: white;
|
||||||
border-radius: 8px;
|
border-radius: 12px;
|
||||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
box-shadow: 0 2px 12px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-top {
|
||||||
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-title {
|
.page-title {
|
||||||
@@ -427,9 +473,15 @@ export default {
|
|||||||
|
|
||||||
.header-actions {
|
.header-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
gap: 15px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.action-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
.add-btn {
|
.add-btn {
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
@@ -437,9 +489,154 @@ export default {
|
|||||||
|
|
||||||
.table-container {
|
.table-container {
|
||||||
background-color: white;
|
background-color: white;
|
||||||
border-radius: 8px;
|
border-radius: 12px;
|
||||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
box-shadow: 0 2px 12px rgba(0,0,0,0.1);
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
display: table;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-header {
|
||||||
|
display: table-row;
|
||||||
|
background: #f8f9fa;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-row {
|
||||||
|
display: table-row;
|
||||||
|
border-bottom: 1px solid #ebeef5;
|
||||||
|
transition: background-color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-row:hover {
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.th, .td {
|
||||||
|
display: table-cell;
|
||||||
|
padding: 12px 15px;
|
||||||
|
text-align: left;
|
||||||
|
vertical-align: middle;
|
||||||
|
border-bottom: 1px solid #ebeef5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.th {
|
||||||
|
background: #f8f9fa;
|
||||||
|
color: #909399;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.td {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #606266;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-buttons-inline {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cards-container {
|
||||||
|
display: grid;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rule-card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 2px 12px rgba(0,0,0,0.1);
|
||||||
|
overflow: hidden;
|
||||||
|
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rule-card:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 20px rgba(0,0,0,0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 20px 20px 15px;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title i {
|
||||||
|
margin-right: 8px;
|
||||||
|
color: #409EFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-content {
|
||||||
|
padding: 15px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px 0;
|
||||||
|
border-bottom: 1px solid #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-row:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-label {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #909399;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-value {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #303133;
|
||||||
|
text-align: right;
|
||||||
|
max-width: 60%;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.speed-value {
|
||||||
|
color: #409EFF;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tunnel-value {
|
||||||
|
color: #67C23A;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-actions {
|
||||||
|
display: flex;
|
||||||
|
padding: 15px 20px 20px;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-actions .el-button {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
text-align: center;
|
||||||
|
padding: 60px 20px;
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 2px 12px rgba(0,0,0,0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-hint {
|
.form-hint {
|
||||||
@@ -452,16 +649,12 @@ export default {
|
|||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty-state {
|
.text-muted {
|
||||||
text-align: center;
|
color: #909399;
|
||||||
padding: 40px;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Element UI 样式调整 */
|
/* Element UI 样式调整 */
|
||||||
.el-table {
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-dialog {
|
.el-dialog {
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
}
|
}
|
||||||
@@ -478,11 +671,6 @@ export default {
|
|||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.text-muted {
|
|
||||||
color: #909399;
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 响应式设计 */
|
/* 响应式设计 */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.speed-limit-container {
|
.speed-limit-container {
|
||||||
@@ -490,18 +678,83 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.page-header {
|
.page-header {
|
||||||
flex-direction: column;
|
padding: 15px;
|
||||||
gap: 15px;
|
}
|
||||||
align-items: stretch;
|
|
||||||
|
.page-title {
|
||||||
|
font-size: 20px;
|
||||||
|
margin-bottom: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-actions {
|
.header-actions {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-buttons {
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-buttons .el-button {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-actions {
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
padding: 15px;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.card-content {
|
||||||
|
padding: 10px 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-row {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-value {
|
||||||
|
max-width: 100%;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-container {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.speed-limit-container {
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
.page-title {
|
.page-title {
|
||||||
text-align: center;
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title i {
|
||||||
|
font-size: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-buttons {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
padding: 40px 15px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -1,13 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="tunnel-container">
|
<div class="tunnel-container">
|
||||||
<!-- 页面头部 -->
|
<el-button
|
||||||
<div class="page-header">
|
|
||||||
<h1 class="page-title">
|
|
||||||
<i class="el-icon-connection"></i>
|
|
||||||
隧道管理
|
|
||||||
</h1>
|
|
||||||
<div class="header-actions">
|
|
||||||
<el-button
|
|
||||||
type="primary"
|
type="primary"
|
||||||
icon="el-icon-plus"
|
icon="el-icon-plus"
|
||||||
@click="handleAdd"
|
@click="handleAdd"
|
||||||
@@ -16,19 +9,8 @@
|
|||||||
新增隧道
|
新增隧道
|
||||||
</el-button>
|
</el-button>
|
||||||
|
|
||||||
<el-button
|
|
||||||
type="success"
|
|
||||||
icon="el-icon-refresh"
|
|
||||||
@click="loadTunnels"
|
|
||||||
:loading="loading"
|
|
||||||
>
|
|
||||||
刷新
|
|
||||||
</el-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 隧道卡片展示 -->
|
<!-- 隧道卡片展示 -->
|
||||||
<div class="cards-container" v-loading="loading">
|
<div class="cards-container" v-loading="loading" style="margin-top: 10px;">
|
||||||
<div class="cards-grid">
|
<div class="cards-grid">
|
||||||
<div
|
<div
|
||||||
v-for="tunnel in tunnelList"
|
v-for="tunnel in tunnelList"
|
||||||
@@ -52,6 +34,17 @@
|
|||||||
<el-tag v-if="tunnel.flow === 1" type="success" size="mini">单向计算</el-tag>
|
<el-tag v-if="tunnel.flow === 1" type="success" size="mini">单向计算</el-tag>
|
||||||
<el-tag v-else type="info" size="mini">双向计算</el-tag>
|
<el-tag v-else type="info" size="mini">双向计算</el-tag>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<el-button
|
||||||
|
style="margin-left: 10px;"
|
||||||
|
size="small"
|
||||||
|
type="danger"
|
||||||
|
icon="el-icon-delete"
|
||||||
|
@click="handleDelete(tunnel)"
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 流程图 -->
|
<!-- 流程图 -->
|
||||||
@@ -61,7 +54,9 @@
|
|||||||
<div class="flow-icon">
|
<div class="flow-icon">
|
||||||
<i class="el-icon-monitor"></i>
|
<i class="el-icon-monitor"></i>
|
||||||
</div>
|
</div>
|
||||||
<div class="flow-label">客户端</div>
|
<div class="flow-content">
|
||||||
|
<div class="flow-label">客户端</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 箭头 -->
|
<!-- 箭头 -->
|
||||||
@@ -74,15 +69,17 @@
|
|||||||
<div class="flow-icon">
|
<div class="flow-icon">
|
||||||
<i class="el-icon-upload"></i>
|
<i class="el-icon-upload"></i>
|
||||||
</div>
|
</div>
|
||||||
<div class="flow-label">入口</div>
|
<div class="flow-content">
|
||||||
<div class="flow-detail">
|
<div class="flow-label">入口</div>
|
||||||
<div class="detail-item">
|
<div class="flow-detail">
|
||||||
<span class="label">IP:</span>
|
<div class="detail-item">
|
||||||
<span class="value">{{ tunnel.inIp }}</span>
|
<span class="label">IP:</span>
|
||||||
</div>
|
<span class="value" :title="tunnel.inIp">{{ tunnel.inIp }}</span>
|
||||||
<div class="detail-item">
|
</div>
|
||||||
<span class="label">端口:</span>
|
<div class="detail-item">
|
||||||
<span class="value">{{ tunnel.inPortSta === tunnel.inPortEnd ? tunnel.inPortSta : `${tunnel.inPortSta}-${tunnel.inPortEnd}` }}</span>
|
<span class="label">端口:</span>
|
||||||
|
<span class="value">{{ tunnel.inPortSta === tunnel.inPortEnd ? tunnel.inPortSta : `${tunnel.inPortSta}-${tunnel.inPortEnd}` }}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -97,15 +94,19 @@
|
|||||||
<div class="flow-icon">
|
<div class="flow-icon">
|
||||||
<i class="el-icon-download"></i>
|
<i class="el-icon-download"></i>
|
||||||
</div>
|
</div>
|
||||||
<div class="flow-label">出口</div>
|
<div class="flow-content">
|
||||||
<div class="flow-detail">
|
<div class="flow-label" >出口</div>
|
||||||
<div class="detail-item">
|
<div class="flow-detail">
|
||||||
<span class="label">IP:</span>
|
|
||||||
<span class="value">{{ tunnel.outIp }}</span>
|
<div class="detail-item">
|
||||||
</div>
|
<span class="label">IP:</span>
|
||||||
<div class="detail-item">
|
<span class="value" :title="tunnel.outIp">{{ tunnel.outIp }}</span>
|
||||||
<span class="label">端口:</span>
|
</div>
|
||||||
<span class="value">{{ tunnel.outIpSta === tunnel.outIpEnd ? tunnel.outIpSta : `${tunnel.outIpSta}-${tunnel.outIpEnd}` }}</span>
|
|
||||||
|
<div class="detail-item">
|
||||||
|
<span class="label">端口:</span>
|
||||||
|
<span class="value">{{ tunnel.outIpSta === tunnel.outIpEnd ? tunnel.outIpSta : `${tunnel.outIpSta}-${tunnel.outIpEnd}` }}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -120,10 +121,12 @@
|
|||||||
<div class="flow-icon">
|
<div class="flow-icon">
|
||||||
<i class="el-icon-position"></i>
|
<i class="el-icon-position"></i>
|
||||||
</div>
|
</div>
|
||||||
<div class="flow-label">目标</div>
|
<div class="flow-content">
|
||||||
<div class="flow-detail">
|
<div class="flow-label">目标</div>
|
||||||
<div class="detail-item">
|
<div class="flow-detail">
|
||||||
<span class="label">目标服务</span>
|
<div class="detail-item">
|
||||||
|
<span class="label">目标服务</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -136,18 +139,6 @@
|
|||||||
<span>{{ tunnel.createdTime | dateFormat }}</span>
|
<span>{{ tunnel.createdTime | dateFormat }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 操作按钮 -->
|
|
||||||
<div class="tunnel-actions">
|
|
||||||
<el-button
|
|
||||||
size="small"
|
|
||||||
type="danger"
|
|
||||||
icon="el-icon-delete"
|
|
||||||
@click="handleDelete(tunnel)"
|
|
||||||
>
|
|
||||||
删除
|
|
||||||
</el-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -155,7 +146,6 @@
|
|||||||
<div v-if="!loading && tunnelList.length === 0" class="empty-state">
|
<div v-if="!loading && tunnelList.length === 0" class="empty-state">
|
||||||
<i class="el-icon-connection"></i>
|
<i class="el-icon-connection"></i>
|
||||||
<p>暂无隧道数据</p>
|
<p>暂无隧道数据</p>
|
||||||
<el-button type="primary" @click="handleAdd">创建第一个隧道</el-button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -763,6 +753,7 @@ export default {
|
|||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: #606266;
|
color: #606266;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.flow-detail {
|
.flow-detail {
|
||||||
@@ -844,38 +835,386 @@ export default {
|
|||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 响应式设计 */
|
/* 中等屏幕优化 (768px - 1024px) */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 1024px) {
|
||||||
.cards-grid {
|
.cards-grid {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: repeat(auto-fit, minmax(500px, 1fr));
|
||||||
gap: 15px;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.tunnel-flow {
|
/* 平板优化 (480px - 768px) */
|
||||||
flex-direction: column;
|
@media (max-width: 768px) {
|
||||||
gap: 15px;
|
.tunnel-container {
|
||||||
}
|
padding: 15px;
|
||||||
|
|
||||||
.flow-arrow {
|
|
||||||
transform: rotate(90deg);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-header {
|
.page-header {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
gap: 15px;
|
gap: 15px;
|
||||||
|
padding: 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title {
|
||||||
|
font-size: 22px;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-actions {
|
.header-actions {
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.cards-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
.tunnel-card {
|
.tunnel-card {
|
||||||
padding: 15px;
|
padding: 20px;
|
||||||
|
min-height: auto;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tunnel-header {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tunnel-name {
|
.tunnel-name {
|
||||||
margin-right: 80px;
|
font-size: 17px;
|
||||||
|
margin: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tunnel-badges {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: flex-start;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 平板端流程图优化 */
|
||||||
|
.tunnel-flow {
|
||||||
|
padding: 18px 15px;
|
||||||
|
margin-bottom: 18px;
|
||||||
|
overflow-x: auto;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flow-item {
|
||||||
|
min-width: 90px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flow-icon {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
font-size: 16px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flow-label {
|
||||||
|
font-size: 13px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-item {
|
||||||
|
font-size: 11px;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-item .value {
|
||||||
|
font-size: 12px;
|
||||||
|
display: inline-block;
|
||||||
|
max-width: 140px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flow-arrow {
|
||||||
|
font-size: 14px;
|
||||||
|
margin: 0 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tunnel-meta {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
font-size: 11px;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 手机端优化 (最大 480px) */
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.tunnel-container {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
padding: 15px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-actions {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-actions .el-button {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tunnel-card {
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tunnel-header {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tunnel-name {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tunnel-badges {
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tunnel-badges .el-tag {
|
||||||
|
font-size: 10px;
|
||||||
|
padding: 2px 6px;
|
||||||
|
height: auto;
|
||||||
|
line-height: 1.3;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 手机端流程图 - 垂直布局 */
|
||||||
|
.tunnel-flow {
|
||||||
|
padding: 12px 8px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flow-item {
|
||||||
|
min-width: auto;
|
||||||
|
width: 100%;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: white;
|
||||||
|
border-radius: 6px;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.flow-icon {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-bottom: 0;
|
||||||
|
margin-right: 10px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flow-content {
|
||||||
|
flex: 1;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flow-label {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
color: #303133;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flow-detail {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-item {
|
||||||
|
display: block;
|
||||||
|
font-size: 10px;
|
||||||
|
margin-bottom: 3px;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-item .label {
|
||||||
|
display: inline;
|
||||||
|
margin-right: 4px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-item .value {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #409eff;
|
||||||
|
display: inline-block;
|
||||||
|
max-width: 120px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
vertical-align: top;
|
||||||
|
cursor: help;
|
||||||
|
border-bottom: 1px dotted transparent;
|
||||||
|
transition: border-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-item .value:hover {
|
||||||
|
border-bottom-color: #409eff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flow-arrow {
|
||||||
|
display: none; /* 垂直布局时隐藏箭头 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 删除按钮优化 */
|
||||||
|
.tunnel-header .el-button {
|
||||||
|
padding: 6px 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-left: 0;
|
||||||
|
margin-top: 8px;
|
||||||
|
align-self: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tunnel-meta {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-size: 10px;
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-item {
|
||||||
|
margin-right: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-item i {
|
||||||
|
margin-right: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 空状态手机端优化 */
|
||||||
|
.empty-state {
|
||||||
|
padding: 30px 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state i {
|
||||||
|
font-size: 40px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state p {
|
||||||
|
font-size: 14px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 超小屏幕优化 (最大 360px) */
|
||||||
|
@media (max-width: 360px) {
|
||||||
|
.tunnel-container {
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tunnel-card {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tunnel-name {
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tunnel-badges .el-tag {
|
||||||
|
font-size: 9px;
|
||||||
|
padding: 1px 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flow-item {
|
||||||
|
padding: 6px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flow-icon {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
font-size: 12px;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flow-label {
|
||||||
|
font-size: 11px;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-item {
|
||||||
|
font-size: 9px;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-item .value {
|
||||||
|
font-size: 10px;
|
||||||
|
display: inline-block;
|
||||||
|
max-width: 100px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 横屏模式优化 */
|
||||||
|
@media (max-width: 768px) and (orientation: landscape) {
|
||||||
|
.tunnel-flow {
|
||||||
|
flex-direction: row;
|
||||||
|
overflow-x: auto;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flow-item {
|
||||||
|
flex-direction: column;
|
||||||
|
min-width: 80px;
|
||||||
|
width: auto;
|
||||||
|
padding: 8px;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flow-icon {
|
||||||
|
margin-right: 0;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flow-content {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flow-detail {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flow-arrow {
|
||||||
|
display: flex;
|
||||||
|
margin: 0 4px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
+670
-136
@@ -28,98 +28,185 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 用户列表 -->
|
<!-- 用户列表 -->
|
||||||
<div class="table-container">
|
<div class="list-container">
|
||||||
<el-table
|
<!-- 桌面端表格视图 -->
|
||||||
:data="userList"
|
<div class="desktop-view">
|
||||||
v-loading="loading"
|
<el-table
|
||||||
stripe
|
:data="userList"
|
||||||
border
|
v-loading="loading"
|
||||||
style="width: 100%"
|
stripe
|
||||||
>
|
border
|
||||||
<el-table-column prop="id" label="ID"></el-table-column>
|
style="width: 100%"
|
||||||
<el-table-column prop="name" label="姓名"></el-table-column>
|
>
|
||||||
<el-table-column prop="user" label="用户名"></el-table-column>
|
<el-table-column prop="id" label="ID" width="60"></el-table-column>
|
||||||
<el-table-column label="流量统计" width="220">
|
<el-table-column prop="name" label="姓名" min-width="100"></el-table-column>
|
||||||
<template slot-scope="scope">
|
<el-table-column prop="user" label="用户名" min-width="120"></el-table-column>
|
||||||
<div class="flow-stats">
|
<el-table-column label="流量统计" width="200">
|
||||||
<div class="flow-item">
|
<template slot-scope="scope">
|
||||||
<span class="flow-label">总流量:</span>
|
<div class="flow-stats">
|
||||||
<span class="flow-value">{{ formatFlow(scope.row.flow, 'gb') }}</span>
|
<div class="flow-item">
|
||||||
|
<span class="flow-label">总量:</span>
|
||||||
|
<span class="flow-value">{{ formatFlow(scope.row.flow, 'gb') }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flow-item">
|
||||||
|
<span class="flow-label">已用:</span>
|
||||||
|
<span class="flow-value used">{{ formatFlow(calculateUserTotalUsedFlow(scope.row)) }}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flow-item">
|
</template>
|
||||||
<span class="flow-label">已用:</span>
|
</el-table-column>
|
||||||
<span class="flow-value used">{{ formatFlow(calculateUserTotalUsedFlow(scope.row)) }}</span>
|
<el-table-column prop="num" label="转发数量" width="100"></el-table-column>
|
||||||
|
<el-table-column prop="status" label="状态" width="80">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-tag :type="scope.row.status === 1 ? 'success' : 'danger'" size="small">
|
||||||
|
{{ scope.row.status === 1 ? '正常' : '禁用' }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="expTime" label="过期时间" width="180">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<span v-if="scope.row.expTime" :class="getExpTimeClass(scope.row.expTime)">
|
||||||
|
{{ scope.row.expTime | dateFormat }}
|
||||||
|
</span>
|
||||||
|
<span v-else class="no-exptime">永久</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="240">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-button
|
||||||
|
size="mini"
|
||||||
|
type="primary"
|
||||||
|
icon="el-icon-edit"
|
||||||
|
@click="handleEdit(scope.row)"
|
||||||
|
>
|
||||||
|
编辑
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
size="mini"
|
||||||
|
type="success"
|
||||||
|
icon="el-icon-s-tools"
|
||||||
|
@click="handleAssignTunnel(scope.row)"
|
||||||
|
>
|
||||||
|
分配
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
size="mini"
|
||||||
|
type="danger"
|
||||||
|
icon="el-icon-delete"
|
||||||
|
@click="handleDelete(scope.row)"
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 移动端卡片视图 -->
|
||||||
|
<div class="mobile-view" v-loading="loading">
|
||||||
|
<div v-if="userList.length === 0 && !loading" class="empty-state">
|
||||||
|
<el-empty description="暂无用户数据"></el-empty>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-for="user in userList" :key="user.id" class="user-card">
|
||||||
|
<!-- 卡片头部 -->
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="user-info">
|
||||||
|
<div class="user-name">{{ user.name }}</div>
|
||||||
|
<div class="user-account">@{{ user.user }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="user-status">
|
||||||
|
<el-tag :type="user.status === 1 ? 'success' : 'danger'" size="small">
|
||||||
|
{{ user.status === 1 ? '正常' : '禁用' }}
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 卡片内容 -->
|
||||||
|
<div class="card-content">
|
||||||
|
<!-- 流量信息 -->
|
||||||
|
<div class="info-section">
|
||||||
|
<div class="section-title">流量统计</div>
|
||||||
|
<div class="flow-info">
|
||||||
|
<div class="flow-item">
|
||||||
|
<span class="label">总流量</span>
|
||||||
|
<span class="value primary">{{ formatFlow(user.flow, 'gb') }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flow-item">
|
||||||
|
<span class="label">已使用</span>
|
||||||
|
<span class="value danger">{{ formatFlow(calculateUserTotalUsedFlow(user)) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flow-item">
|
||||||
|
<span class="label">转发数量</span>
|
||||||
|
<span class="value">{{ user.num }}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
|
||||||
</el-table-column>
|
<!-- 时间信息 -->
|
||||||
<el-table-column prop="num" label="转发数量" width="100"></el-table-column>
|
<div class="info-section">
|
||||||
<el-table-column prop="status" label="状态">
|
<div class="section-title">时间信息</div>
|
||||||
<template slot-scope="scope">
|
<div class="time-info">
|
||||||
<el-tag :type="scope.row.status === 1 ? 'success' : 'danger'">
|
<div class="time-item">
|
||||||
{{ scope.row.status === 1 ? '正常' : '禁用' }}
|
<span class="label">过期时间</span>
|
||||||
</el-tag>
|
<span v-if="user.expTime" :class="['value', getExpTimeClass(user.expTime)]">
|
||||||
</template>
|
{{ user.expTime | dateFormat }}
|
||||||
</el-table-column>
|
</span>
|
||||||
<el-table-column prop="flowResetTime" label="流量重置日期" width="130">
|
<span v-else class="value no-exptime">永久</span>
|
||||||
<template slot-scope="scope">
|
</div>
|
||||||
<span v-if="scope.row.flowResetTime">
|
<div class="time-item">
|
||||||
每月{{ scope.row.flowResetTime }}号
|
<span class="label">流量重置</span>
|
||||||
</span>
|
<span class="value">
|
||||||
<span v-else class="no-reset-time">未设置</span>
|
<span v-if="user.flowResetTime">每月{{ user.flowResetTime }}号</span>
|
||||||
</template>
|
<span v-else class="no-reset-time">未设置</span>
|
||||||
</el-table-column>
|
</span>
|
||||||
<el-table-column prop="expTime" label="过期时间" width="180">
|
</div>
|
||||||
<template slot-scope="scope">
|
<div class="time-item">
|
||||||
<span v-if="scope.row.expTime" :class="getExpTimeClass(scope.row.expTime)">
|
<span class="label">创建时间</span>
|
||||||
{{ scope.row.expTime | dateFormat }}
|
<span class="value">{{ user.createdTime | dateFormat }}</span>
|
||||||
</span>
|
</div>
|
||||||
<span v-else class="no-exptime">永久</span>
|
</div>
|
||||||
</template>
|
</div>
|
||||||
</el-table-column>
|
</div>
|
||||||
<el-table-column prop="createdTime" label="创建时间">
|
|
||||||
<template slot-scope="scope">
|
<!-- 操作按钮 -->
|
||||||
{{ scope.row.createdTime | dateFormat }}
|
<div class="card-actions">
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column prop="updatedTime" label="更新时间">
|
|
||||||
<template slot-scope="scope">
|
|
||||||
{{ scope.row.updatedTime | dateFormat }}
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="操作" width="280">
|
|
||||||
<template slot-scope="scope">
|
|
||||||
<el-button
|
<el-button
|
||||||
size="mini"
|
size="small"
|
||||||
type="primary"
|
type="primary"
|
||||||
icon="el-icon-edit"
|
icon="el-icon-edit"
|
||||||
@click="handleEdit(scope.row)"
|
@click="handleEdit(user)"
|
||||||
|
plain
|
||||||
>
|
>
|
||||||
编辑
|
编辑
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button
|
<el-button
|
||||||
size="mini"
|
size="small"
|
||||||
type="success"
|
type="success"
|
||||||
icon="el-icon-s-tools"
|
icon="el-icon-s-tools"
|
||||||
@click="handleAssignTunnel(scope.row)"
|
@click="handleAssignTunnel(user)"
|
||||||
|
plain
|
||||||
>
|
>
|
||||||
分配
|
分配权限
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button
|
<el-button
|
||||||
size="mini"
|
size="small"
|
||||||
type="danger"
|
type="danger"
|
||||||
icon="el-icon-delete"
|
icon="el-icon-delete"
|
||||||
@click="handleDelete(scope.row)"
|
@click="handleDelete(user)"
|
||||||
|
plain
|
||||||
>
|
>
|
||||||
删除
|
删除
|
||||||
</el-button>
|
</el-button>
|
||||||
</template>
|
</div>
|
||||||
</el-table-column>
|
</div>
|
||||||
</el-table>
|
</div>
|
||||||
|
|
||||||
<!-- 分页组件 -->
|
<!-- 分页组件 -->
|
||||||
<div class="pagination-container">
|
<div class="pagination-container">
|
||||||
|
<!-- 桌面端分页 -->
|
||||||
<el-pagination
|
<el-pagination
|
||||||
|
class="desktop-pagination"
|
||||||
@size-change="handleSizeChange"
|
@size-change="handleSizeChange"
|
||||||
@current-change="handleCurrentChange"
|
@current-change="handleCurrentChange"
|
||||||
:current-page="pagination.current"
|
:current-page="pagination.current"
|
||||||
@@ -129,6 +216,20 @@
|
|||||||
:total="pagination.total"
|
:total="pagination.total"
|
||||||
>
|
>
|
||||||
</el-pagination>
|
</el-pagination>
|
||||||
|
|
||||||
|
<!-- 移动端分页 -->
|
||||||
|
<el-pagination
|
||||||
|
class="mobile-pagination"
|
||||||
|
@size-change="handleSizeChange"
|
||||||
|
@current-change="handleCurrentChange"
|
||||||
|
:current-page="pagination.current"
|
||||||
|
:page-sizes="[10, 20, 50]"
|
||||||
|
:page-size="pagination.size"
|
||||||
|
layout="prev, pager, next"
|
||||||
|
:total="pagination.total"
|
||||||
|
small
|
||||||
|
>
|
||||||
|
</el-pagination>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -170,10 +271,7 @@
|
|||||||
show-password
|
show-password
|
||||||
></el-input>
|
></el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
<el-form-item label="流量(GB)" prop="flow">
|
||||||
<el-row :gutter="20">
|
|
||||||
<el-col :span="12">
|
|
||||||
<el-form-item label="流量(GB)" prop="flow">
|
|
||||||
<el-input-number
|
<el-input-number
|
||||||
v-model="userForm.flow"
|
v-model="userForm.flow"
|
||||||
:min="0"
|
:min="0"
|
||||||
@@ -182,8 +280,7 @@
|
|||||||
style="width: 100%;"
|
style="width: 100%;"
|
||||||
></el-input-number>
|
></el-input-number>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
|
||||||
<el-col :span="12">
|
|
||||||
<el-form-item label="转发数量" prop="num">
|
<el-form-item label="转发数量" prop="num">
|
||||||
<el-input-number
|
<el-input-number
|
||||||
v-model="userForm.num"
|
v-model="userForm.num"
|
||||||
@@ -193,8 +290,6 @@
|
|||||||
style="width: 100%;"
|
style="width: 100%;"
|
||||||
></el-input-number>
|
></el-input-number>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
|
||||||
</el-row>
|
|
||||||
|
|
||||||
<el-form-item label="过期时间" prop="expTime">
|
<el-form-item label="过期时间" prop="expTime">
|
||||||
<el-date-picker
|
<el-date-picker
|
||||||
@@ -377,73 +472,147 @@
|
|||||||
<!-- 已有隧道权限列表 -->
|
<!-- 已有隧道权限列表 -->
|
||||||
<div class="permission-list-section">
|
<div class="permission-list-section">
|
||||||
<h4>已有隧道权限</h4>
|
<h4>已有隧道权限</h4>
|
||||||
<el-table
|
|
||||||
:data="userTunnelList"
|
|
||||||
v-loading="tunnelListLoading"
|
|
||||||
border
|
|
||||||
style="width: 100%"
|
|
||||||
>
|
|
||||||
<el-table-column prop="tunnelName" label="隧道名称"></el-table-column>
|
|
||||||
<el-table-column label="状态" width="80">
|
|
||||||
<template slot-scope="scope">
|
|
||||||
<el-tag :type="scope.row.status === 1 ? 'success' : 'danger'" size="small">
|
|
||||||
{{ scope.row.status === 1 ? '正常' : '禁用' }}
|
|
||||||
</el-tag>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="流量统计" width="180">
|
|
||||||
<template slot-scope="scope">
|
|
||||||
<div class="tunnel-flow-stats">
|
|
||||||
<div class="tunnel-flow-item">
|
|
||||||
<span class="tunnel-flow-label">限制:</span>
|
|
||||||
<span class="tunnel-flow-value">{{ formatFlow(scope.row.flow, 'gb') }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="tunnel-flow-item">
|
|
||||||
<span class="tunnel-flow-label">已用:</span>
|
|
||||||
<span class="tunnel-flow-value used">{{ formatFlow(calculateTunnelUsedFlow(scope.row)) }}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
<!-- 桌面端表格视图 -->
|
||||||
|
<div class="desktop-view">
|
||||||
|
<el-table
|
||||||
|
:data="userTunnelList"
|
||||||
|
v-loading="tunnelListLoading"
|
||||||
|
border
|
||||||
|
style="width: 100%"
|
||||||
|
>
|
||||||
|
<el-table-column prop="tunnelName" label="隧道名称" min-width="120"></el-table-column>
|
||||||
|
<el-table-column label="状态" width="80">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-tag :type="scope.row.status === 1 ? 'success' : 'danger'" size="small">
|
||||||
|
{{ scope.row.status === 1 ? '正常' : '禁用' }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="流量统计" width="180">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<div class="tunnel-flow-stats">
|
||||||
|
<div class="tunnel-flow-item">
|
||||||
|
<span class="tunnel-flow-label">限制:</span>
|
||||||
|
<span class="tunnel-flow-value">{{ formatFlow(scope.row.flow, 'gb') }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="tunnel-flow-item">
|
||||||
|
<span class="tunnel-flow-label">已用:</span>
|
||||||
|
<span class="tunnel-flow-value used">{{ formatFlow(calculateTunnelUsedFlow(scope.row)) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="num" label="转发数量" width="100"></el-table-column>
|
||||||
|
<el-table-column prop="speedLimitName" label="限速规则" width="120">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-tag v-if="scope.row.speedLimitName" type="warning" size="small">
|
||||||
|
{{ scope.row.speedLimitName }}
|
||||||
|
</el-tag>
|
||||||
|
<el-tag v-else type="success" size="small">不限速</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="expTime" label="到期时间" width="150">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
{{ scope.row.expTime | dateFormat }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="120">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-button
|
||||||
|
size="mini"
|
||||||
|
type="primary"
|
||||||
|
icon="el-icon-edit"
|
||||||
|
@click="handleEditTunnelPermission(scope.row)"
|
||||||
|
>
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
size="mini"
|
||||||
|
type="danger"
|
||||||
|
icon="el-icon-delete"
|
||||||
|
@click="handleRemoveTunnelPermission(scope.row)"
|
||||||
|
>
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 移动端卡片视图 -->
|
||||||
|
<div class="mobile-view" v-loading="tunnelListLoading">
|
||||||
|
<div v-if="userTunnelList.length === 0 && !tunnelListLoading" class="empty-state">
|
||||||
|
<el-empty description="暂无隧道权限" :image-size="100"></el-empty>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-for="tunnel in userTunnelList" :key="tunnel.id" class="tunnel-card">
|
||||||
|
<!-- 卡片头部 -->
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="tunnel-info">
|
||||||
|
<div class="tunnel-name">{{ tunnel.tunnelName }}</div>
|
||||||
|
<div class="tunnel-meta">
|
||||||
|
<el-tag :type="tunnel.status === 1 ? 'success' : 'danger'" size="mini">
|
||||||
|
{{ tunnel.status === 1 ? '正常' : '禁用' }}
|
||||||
|
</el-tag>
|
||||||
|
<el-tag v-if="tunnel.speedLimitName" type="warning" size="mini" style="margin-left: 5px;">
|
||||||
|
{{ tunnel.speedLimitName }}
|
||||||
|
</el-tag>
|
||||||
|
<el-tag v-else type="success" size="mini" style="margin-left: 5px;">不限速</el-tag>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</div>
|
||||||
</el-table-column>
|
|
||||||
<el-table-column prop="num" label="转发数量" width="100"></el-table-column>
|
<!-- 卡片内容 -->
|
||||||
<el-table-column prop="speedLimitName" label="限速规则" width="150">
|
<div class="card-content">
|
||||||
<template slot-scope="scope">
|
<div class="tunnel-stats">
|
||||||
<el-tag v-if="scope.row.speedLimitName" type="warning">
|
<div class="stat-item">
|
||||||
{{ scope.row.speedLimitName }}
|
<div class="stat-label">流量限制</div>
|
||||||
</el-tag>
|
<div class="stat-value primary">{{ formatFlow(tunnel.flow, 'gb') }}</div>
|
||||||
<el-tag v-else type="success">不限速</el-tag>
|
</div>
|
||||||
</template>
|
<div class="stat-item">
|
||||||
</el-table-column>
|
<div class="stat-label">已使用</div>
|
||||||
<el-table-column prop="flowResetTime" label="流量重置日期" width="130">
|
<div class="stat-value danger">{{ formatFlow(calculateTunnelUsedFlow(tunnel)) }}</div>
|
||||||
<template slot-scope="scope">
|
</div>
|
||||||
每月{{ scope.row.flowResetTime }}号
|
<div class="stat-item">
|
||||||
</template>
|
<div class="stat-label">转发数量</div>
|
||||||
</el-table-column>
|
<div class="stat-value">{{ tunnel.num }}</div>
|
||||||
<el-table-column prop="expTime" label="到期时间" width="180">
|
</div>
|
||||||
<template slot-scope="scope">
|
</div>
|
||||||
{{ scope.row.expTime | dateFormat }}
|
|
||||||
</template>
|
<div class="tunnel-details">
|
||||||
</el-table-column>
|
<div class="detail-item">
|
||||||
<el-table-column label="操作" width="150">
|
<span class="detail-label">流量重置</span>
|
||||||
<template slot-scope="scope">
|
<span class="detail-value">每月{{ tunnel.flowResetTime }}号</span>
|
||||||
|
</div>
|
||||||
|
<div class="detail-item">
|
||||||
|
<span class="detail-label">到期时间</span>
|
||||||
|
<span class="detail-value">{{ tunnel.expTime | dateFormat }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 操作按钮 -->
|
||||||
|
<div class="card-actions">
|
||||||
<el-button
|
<el-button
|
||||||
size="mini"
|
size="small"
|
||||||
type="primary"
|
type="primary"
|
||||||
icon="el-icon-edit"
|
icon="el-icon-edit"
|
||||||
@click="handleEditTunnelPermission(scope.row)"
|
@click="handleEditTunnelPermission(tunnel)"
|
||||||
|
plain
|
||||||
>
|
>
|
||||||
|
编辑
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button
|
<el-button
|
||||||
size="mini"
|
size="small"
|
||||||
type="danger"
|
type="danger"
|
||||||
icon="el-icon-delete"
|
icon="el-icon-delete"
|
||||||
@click="handleRemoveTunnelPermission(scope.row)"
|
@click="handleRemoveTunnelPermission(tunnel)"
|
||||||
|
plain
|
||||||
>
|
>
|
||||||
|
删除
|
||||||
</el-button>
|
</el-button>
|
||||||
</template>
|
</div>
|
||||||
</el-table-column>
|
</div>
|
||||||
</el-table>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<span slot="footer" class="dialog-footer">
|
<span slot="footer" class="dialog-footer">
|
||||||
@@ -1188,18 +1357,36 @@ export default {
|
|||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-container {
|
.list-container {
|
||||||
background: white;
|
background: white;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 响应式视图控制 */
|
||||||
|
.desktop-view {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-view {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.pagination-container {
|
.pagination-container {
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 分页组件响应式控制 */
|
||||||
|
.desktop-pagination {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-pagination {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
/* 对话框样式调整 */
|
/* 对话框样式调整 */
|
||||||
.dialog-footer {
|
.dialog-footer {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
@@ -1221,6 +1408,197 @@ export default {
|
|||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 移动端卡片样式 */
|
||||||
|
.user-card, .tunnel-card {
|
||||||
|
background: white;
|
||||||
|
border: 1px solid #e6e6e6;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-card:hover, .tunnel-card:hover {
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
padding: 16px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-bottom: 1px solid #e6e6e6;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-info, .tunnel-info {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-name, .tunnel-name {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-account {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tunnel-meta {
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-status {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-content {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-section {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-section:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
padding-bottom: 6px;
|
||||||
|
border-bottom: 1px solid #e6e6e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flow-info, .time-info {
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flow-item, .time-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #666;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value.primary {
|
||||||
|
color: #409eff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value.danger {
|
||||||
|
color: #f56c6c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value.success {
|
||||||
|
color: #67c23a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-actions {
|
||||||
|
padding: 12px 16px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-top: 1px solid #e6e6e6;
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-actions .el-button {
|
||||||
|
flex: 1;
|
||||||
|
margin: 0;
|
||||||
|
min-width: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 隧道卡片特有样式 */
|
||||||
|
.tunnel-stats {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-item {
|
||||||
|
text-align: center;
|
||||||
|
padding: 12px 8px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid #e6e6e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value.primary {
|
||||||
|
color: #409eff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value.danger {
|
||||||
|
color: #f56c6c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tunnel-details {
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-label {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #666;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-value {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
padding: 40px 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
/* 响应式设计 */
|
/* 响应式设计 */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.user-container {
|
.user-container {
|
||||||
@@ -1231,6 +1609,7 @@ export default {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 15px;
|
gap: 15px;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
|
padding: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-actions {
|
.header-actions {
|
||||||
@@ -1240,20 +1619,175 @@ export default {
|
|||||||
|
|
||||||
.page-title {
|
.page-title {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-container {
|
||||||
|
padding: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pagination-container {
|
.pagination-container {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
margin-top: 16px;
|
||||||
|
overflow-x: auto;
|
||||||
|
padding: 0 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 切换到移动端视图 */
|
||||||
|
.desktop-view {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-view {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 分页组件移动端切换 */
|
||||||
|
.desktop-pagination {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-pagination {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 移动端搜索框优化 */
|
||||||
|
.header-actions .el-input {
|
||||||
|
width: 100% !important;
|
||||||
|
margin-right: 0 !important;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 移动端分页优化 */
|
||||||
|
.mobile-pagination {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-pagination >>> .el-pager {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-pagination >>> .el-pager li {
|
||||||
|
min-width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
line-height: 30px;
|
||||||
|
margin: 0 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-pagination >>> .btn-prev,
|
||||||
|
.mobile-pagination >>> .btn-next {
|
||||||
|
min-width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
line-height: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 移动端对话框优化 */
|
||||||
|
.el-dialog {
|
||||||
|
width: 95% !important;
|
||||||
|
margin-top: 5vh !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.assign-section h4,
|
||||||
|
.permission-list-section h4 {
|
||||||
|
font-size: 16px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 移动端表单布局优化 - 分配新隧道权限部分 */
|
||||||
|
.assign-section .el-row {
|
||||||
|
flex-direction: column !important;
|
||||||
|
margin-left: 0 !important;
|
||||||
|
margin-right: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.assign-section .el-col {
|
||||||
|
width: 100% !important;
|
||||||
|
padding-left: 0 !important;
|
||||||
|
padding-right: 0 !important;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.assign-section .el-col:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 移动端编辑隧道权限表单布局优化 */
|
||||||
|
.el-dialog .el-row {
|
||||||
|
flex-direction: column !important;
|
||||||
|
margin-left: 0 !important;
|
||||||
|
margin-right: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-dialog .el-col {
|
||||||
|
width: 100% !important;
|
||||||
|
padding-left: 0 !important;
|
||||||
|
padding-right: 0 !important;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-dialog .el-col:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 隧道统计在小屏幕上的优化 */
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.tunnel-stats {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
text-align: left;
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-actions {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-actions .el-button {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 超小屏幕分页优化 */
|
||||||
|
.mobile-pagination >>> .el-pager li {
|
||||||
|
min-width: 25px;
|
||||||
|
height: 25px;
|
||||||
|
line-height: 25px;
|
||||||
|
margin: 0 1px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-pagination >>> .btn-prev,
|
||||||
|
.mobile-pagination >>> .btn-next {
|
||||||
|
min-width: 25px;
|
||||||
|
height: 25px;
|
||||||
|
line-height: 25px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-container {
|
||||||
|
padding: 0 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 表格样式优化 */
|
/* 表格样式优化 */
|
||||||
.table-container >>> .el-table th {
|
.list-container >>> .el-table th {
|
||||||
background-color: #fafafa;
|
background-color: #fafafa;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-container >>> .el-table--border td,
|
.list-container >>> .el-table--border td,
|
||||||
.table-container >>> .el-table--border th {
|
.list-container >>> .el-table--border th {
|
||||||
border-right: 1px solid #ebeef5;
|
border-right: 1px solid #ebeef5;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1266,7 +1800,7 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* 按钮样式调整 */
|
/* 按钮样式调整 */
|
||||||
.table-container >>> .el-button--mini {
|
.list-container >>> .el-button--mini {
|
||||||
padding: 5px 10px;
|
padding: 5px 10px;
|
||||||
margin: 0 2px;
|
margin: 0 2px;
|
||||||
}
|
}
|
||||||
|
|||||||
+139
-28
@@ -1,13 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="node-container">
|
<div class="node-container">
|
||||||
<!-- 页面头部 -->
|
<el-button
|
||||||
<div class="page-header">
|
|
||||||
<h1 class="page-title">
|
|
||||||
<i class="el-icon-s-platform"></i>
|
|
||||||
节点管理
|
|
||||||
</h1>
|
|
||||||
<div class="header-actions">
|
|
||||||
<el-button
|
|
||||||
type="primary"
|
type="primary"
|
||||||
icon="el-icon-plus"
|
icon="el-icon-plus"
|
||||||
@click="handleAdd"
|
@click="handleAdd"
|
||||||
@@ -16,20 +9,8 @@
|
|||||||
新增节点
|
新增节点
|
||||||
</el-button>
|
</el-button>
|
||||||
|
|
||||||
<el-button
|
|
||||||
type="success"
|
|
||||||
icon="el-icon-refresh"
|
|
||||||
@click="loadNodes"
|
|
||||||
:loading="loading"
|
|
||||||
>
|
|
||||||
刷新
|
|
||||||
</el-button>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 节点卡片展示 -->
|
<!-- 节点卡片展示 -->
|
||||||
<div class="cards-container" v-loading="loading">
|
<div class="cards-container" v-loading="loading" style="margin-top: 10px;">
|
||||||
<div class="cards-grid">
|
<div class="cards-grid">
|
||||||
<div
|
<div
|
||||||
v-for="node in nodeList"
|
v-for="node in nodeList"
|
||||||
@@ -63,7 +44,7 @@
|
|||||||
<div class="chart-wrapper">
|
<div class="chart-wrapper">
|
||||||
<v-chart
|
<v-chart
|
||||||
:option="getCpuChartOption(node.connectionStatus === 'online' ? (node.systemInfo?.cpuUsage || 0) : 0, node.connectionStatus !== 'online')"
|
:option="getCpuChartOption(node.connectionStatus === 'online' ? (node.systemInfo?.cpuUsage || 0) : 0, node.connectionStatus !== 'online')"
|
||||||
style="height: 120px; width: 100%;"
|
:style="chartStyle"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -73,7 +54,7 @@
|
|||||||
<div class="chart-wrapper">
|
<div class="chart-wrapper">
|
||||||
<v-chart
|
<v-chart
|
||||||
:option="getMemoryChartOption(node.connectionStatus === 'online' ? (node.systemInfo?.memoryUsage || 0) : 0, node.connectionStatus !== 'online')"
|
:option="getMemoryChartOption(node.connectionStatus === 'online' ? (node.systemInfo?.memoryUsage || 0) : 0, node.connectionStatus !== 'online')"
|
||||||
style="height: 120px; width: 100%;"
|
:style="chartStyle"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -340,6 +321,21 @@ export default {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
// 动态图表样式
|
||||||
|
chartStyle() {
|
||||||
|
return {
|
||||||
|
height: this.isMobile ? '80px' : '120px',
|
||||||
|
width: '100%'
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
// 检查是否为移动端
|
||||||
|
isMobile() {
|
||||||
|
return window.innerWidth <= 768;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
filters: {
|
filters: {
|
||||||
dateFormat(timestamp) {
|
dateFormat(timestamp) {
|
||||||
if (!timestamp) return '-';
|
if (!timestamp) return '-';
|
||||||
@@ -351,6 +347,8 @@ export default {
|
|||||||
mounted() {
|
mounted() {
|
||||||
this.loadNodes();
|
this.loadNodes();
|
||||||
this.initWebSocket();
|
this.initWebSocket();
|
||||||
|
// 监听窗口大小变化
|
||||||
|
window.addEventListener('resize', this.handleResize);
|
||||||
},
|
},
|
||||||
|
|
||||||
// 组件激活时(如果使用keep-alive)
|
// 组件激活时(如果使用keep-alive)
|
||||||
@@ -369,6 +367,8 @@ export default {
|
|||||||
// 组件销毁前
|
// 组件销毁前
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
this.closeWebSocket();
|
this.closeWebSocket();
|
||||||
|
// 移除窗口大小变化监听
|
||||||
|
window.removeEventListener('resize', this.handleResize);
|
||||||
},
|
},
|
||||||
|
|
||||||
// 路由离开守卫
|
// 路由离开守卫
|
||||||
@@ -870,6 +870,12 @@ export default {
|
|||||||
const i = Math.floor(Math.log(bytesPerSecond) / Math.log(k));
|
const i = Math.floor(Math.log(bytesPerSecond) / Math.log(k));
|
||||||
|
|
||||||
return parseFloat((bytesPerSecond / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
return parseFloat((bytesPerSecond / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||||
|
},
|
||||||
|
|
||||||
|
// 处理窗口大小变化
|
||||||
|
handleResize() {
|
||||||
|
// 触发计算属性重新计算
|
||||||
|
this.$forceUpdate();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -877,7 +883,7 @@ export default {
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.node-container {
|
.node-container {
|
||||||
padding: 20px;
|
padding: 5px;
|
||||||
background-color: #f5f7fa;
|
background-color: #f5f7fa;
|
||||||
min-height: calc(100vh - 60px);
|
min-height: calc(100vh - 60px);
|
||||||
}
|
}
|
||||||
@@ -1177,23 +1183,32 @@ export default {
|
|||||||
|
|
||||||
/* 响应式设计 */
|
/* 响应式设计 */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
|
.node-container {
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
.cards-grid {
|
.cards-grid {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
gap: 15px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.charts-container {
|
.charts-container {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.traffic-stats {
|
.traffic-stats {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
|
gap: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-header {
|
.page-header {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
gap: 15px;
|
gap: 10px;
|
||||||
|
padding: 15px 20px;
|
||||||
|
margin-bottom: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-actions {
|
.header-actions {
|
||||||
@@ -1201,7 +1216,55 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.node-card {
|
.node-card {
|
||||||
padding: 15px;
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-header {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-name {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-indicator {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-wrapper {
|
||||||
|
padding: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-title {
|
||||||
|
font-size: 12px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-item {
|
||||||
|
padding: 6px;
|
||||||
|
min-height: 45px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-header {
|
||||||
|
font-size: 11px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.traffic-value {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.speed-value {
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ip-value {
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-actions {
|
||||||
|
padding-top: 10px;
|
||||||
|
gap: 6px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1216,13 +1279,61 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 480px) {
|
@media (max-width: 480px) {
|
||||||
|
.node-container {
|
||||||
|
padding: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cards-grid {
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-card {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.charts-container {
|
||||||
|
gap: 6px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.traffic-stats {
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-item {
|
||||||
|
padding: 5px;
|
||||||
|
min-height: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-wrapper {
|
||||||
|
padding: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
.node-actions {
|
.node-actions {
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.node-actions .el-button {
|
.node-actions .el-button {
|
||||||
|
font-size: 11px;
|
||||||
|
padding: 5px 10px;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-btn {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
padding: 7px 12px;
|
padding: 8px 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
padding: 10px 15px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title {
|
||||||
|
font-size: 20px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user