mirror of
https://github.com/Sagit-chu/flvx.git
synced 2026-10-04 01:06:36 +08:00
优化ui
This commit is contained in:
@@ -272,6 +272,19 @@ public class WebSocketServer extends TextWebSocketHandler {
|
|||||||
|
|
||||||
|
|
||||||
public static GostDto send_msg(Long node_id, Object msg, String type) {
|
public static GostDto send_msg(Long node_id, Object msg, String type) {
|
||||||
|
return send_msg(node_id, msg, type, 10); // 默认10秒超时
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送消息到指定节点(支持自定义超时时间)
|
||||||
|
*
|
||||||
|
* @param node_id 节点ID
|
||||||
|
* @param msg 消息内容
|
||||||
|
* @param type 消息类型
|
||||||
|
* @param timeoutSeconds 超时时间(秒)
|
||||||
|
* @return 响应结果
|
||||||
|
*/
|
||||||
|
public static GostDto send_msg(Long node_id, Object msg, String type, int timeoutSeconds) {
|
||||||
WebSocketSession nodeSession = nodeSessions.get(node_id);
|
WebSocketSession nodeSession = nodeSessions.get(node_id);
|
||||||
|
|
||||||
if (nodeSession == null) {
|
if (nodeSession == null) {
|
||||||
@@ -301,7 +314,7 @@ public class WebSocketServer extends TextWebSocketHandler {
|
|||||||
data.put("data", msg);
|
data.put("data", msg);
|
||||||
data.put("requestId", requestId);
|
data.put("requestId", requestId);
|
||||||
sendToUser(nodeSession, data.toJSONString());
|
sendToUser(nodeSession, data.toJSONString());
|
||||||
GostDto result = future.get(10, TimeUnit.SECONDS);
|
GostDto result = future.get(timeoutSeconds, TimeUnit.SECONDS);
|
||||||
return result;
|
return result;
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
|||||||
@@ -727,14 +727,15 @@ public class TunnelServiceImpl extends ServiceImpl<TunnelMapper, Tunnel> impleme
|
|||||||
// 3. 根据隧道类型执行不同的诊断策略
|
// 3. 根据隧道类型执行不同的诊断策略
|
||||||
if (tunnel.getType() == TUNNEL_TYPE_PORT_FORWARD) {
|
if (tunnel.getType() == TUNNEL_TYPE_PORT_FORWARD) {
|
||||||
// 端口转发:只给入口节点发送诊断指令,ping谷歌DNS
|
// 端口转发:只给入口节点发送诊断指令,ping谷歌DNS
|
||||||
DiagnosisResult inResult = performPingDiagnosis(inNode, "8.8.8.8", "入口->外网");
|
DiagnosisResult inResult = performPingDiagnosisWithConnectionCheck(inNode, "8.8.8.8", "入口->外网");
|
||||||
results.add(inResult);
|
results.add(inResult);
|
||||||
} else {
|
} else {
|
||||||
// 隧道转发:入口ping出口,出口ping谷歌DNS
|
// 隧道转发:入口ping出口,出口ping谷歌DNS
|
||||||
DiagnosisResult inToOutResult = performPingDiagnosis(inNode, outNode.getServerIp(), "入口->出口");
|
DiagnosisResult inToOutResult = performPingDiagnosisWithConnectionCheck(inNode, outNode.getServerIp(), "入口->出口");
|
||||||
results.add(inToOutResult);
|
results.add(inToOutResult);
|
||||||
|
|
||||||
DiagnosisResult outToExternalResult = performPingDiagnosis(outNode, "8.8.8.8", "出口->外网");
|
// 先检查出口节点的真实连接状态,然后再进行诊断
|
||||||
|
DiagnosisResult outToExternalResult = performPingDiagnosisWithConnectionCheck(outNode, "8.8.8.8", "出口->外网");
|
||||||
results.add(outToExternalResult);
|
results.add(outToExternalResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -828,6 +829,85 @@ public class TunnelServiceImpl extends ServiceImpl<TunnelMapper, Tunnel> impleme
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行ping诊断(带连接状态检查)
|
||||||
|
*
|
||||||
|
* @param node 执行ping的节点
|
||||||
|
* @param targetIp 目标IP地址
|
||||||
|
* @param description 诊断描述
|
||||||
|
* @return 诊断结果
|
||||||
|
*/
|
||||||
|
private DiagnosisResult performPingDiagnosisWithConnectionCheck(Node node, String targetIp, String description) {
|
||||||
|
DiagnosisResult result = new DiagnosisResult();
|
||||||
|
result.setNodeId(node.getId());
|
||||||
|
result.setNodeName(node.getName());
|
||||||
|
result.setTargetIp(targetIp);
|
||||||
|
result.setDescription(description);
|
||||||
|
result.setTimestamp(System.currentTimeMillis());
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 首先检查节点的WebSocket连接状态
|
||||||
|
if (!isNodeWebSocketConnected(node.getId())) {
|
||||||
|
// 如果WebSocket连接有问题,更新节点状态并返回错误
|
||||||
|
node.setStatus(0); // 标记为离线
|
||||||
|
nodeService.updateById(node);
|
||||||
|
|
||||||
|
result.setSuccess(false);
|
||||||
|
result.setMessage(String.format("节点 %s 实际已离线(WebSocket连接断开),已更新节点状态。请重新启动节点或检查网络连接", node.getName()));
|
||||||
|
result.setAverageTime(-1.0);
|
||||||
|
result.setPacketLoss(100.0);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果连接正常,执行标准的ping诊断
|
||||||
|
DiagnosisResult pingResult = performPingDiagnosis(node, targetIp, description);
|
||||||
|
|
||||||
|
// 如果ping失败,添加更详细的错误信息
|
||||||
|
if (!pingResult.isSuccess() && pingResult.getMessage() != null) {
|
||||||
|
if (pingResult.getMessage().contains("等待响应超时")) {
|
||||||
|
pingResult.setMessage(String.format("节点 %s 响应超时,可能网络延迟较高或负载过重", node.getName()));
|
||||||
|
} else if (pingResult.getMessage().contains("节点无响应")) {
|
||||||
|
pingResult.setMessage(String.format("节点 %s 无响应,请检查节点状态", node.getName()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return pingResult;
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
result.setSuccess(false);
|
||||||
|
result.setMessage(String.format("诊断节点 %s 时发生异常: %s", node.getName(), e.getMessage()));
|
||||||
|
result.setAverageTime(-1.0);
|
||||||
|
result.setPacketLoss(100.0);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查节点的WebSocket连接是否有效
|
||||||
|
*
|
||||||
|
* @param nodeId 节点ID
|
||||||
|
* @return 连接是否有效
|
||||||
|
*/
|
||||||
|
private boolean isNodeWebSocketConnected(Long nodeId) {
|
||||||
|
try {
|
||||||
|
// 发送一个简单的测试ping,超时时间设为3秒
|
||||||
|
JSONObject testData = new JSONObject();
|
||||||
|
testData.put("ip", "127.0.0.1");
|
||||||
|
testData.put("count", 1);
|
||||||
|
|
||||||
|
GostDto testResult = WebSocketServer.send_msg(nodeId, testData, "Ping", 3);
|
||||||
|
|
||||||
|
// 如果能收到任何响应(不管是成功还是失败),说明WebSocket连接正常
|
||||||
|
return testResult != null &&
|
||||||
|
!testResult.getMsg().equals("节点不在线") &&
|
||||||
|
!testResult.getMsg().equals("节点连接已断开") &&
|
||||||
|
!testResult.getMsg().equals("等待响应超时");
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ========== 内部数据类 ==========
|
// ========== 内部数据类 ==========
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export default {
|
|||||||
return new Promise(function(resolve, reject) {
|
return new Promise(function(resolve, reject) {
|
||||||
axios.get(path, {
|
axios.get(path, {
|
||||||
params: data,
|
params: data,
|
||||||
timeout: 12000,
|
timeout: 30000,
|
||||||
headers: {
|
headers: {
|
||||||
"Authorization": window.localStorage.getItem('token')
|
"Authorization": window.localStorage.getItem('token')
|
||||||
}
|
}
|
||||||
@@ -53,7 +53,7 @@ export default {
|
|||||||
post: function(path = '', data = {}) {
|
post: function(path = '', data = {}) {
|
||||||
return new Promise(function(resolve, reject) {
|
return new Promise(function(resolve, reject) {
|
||||||
axios.post(path, data, {
|
axios.post(path, data, {
|
||||||
timeout: 12000,
|
timeout: 30000,
|
||||||
headers:{
|
headers:{
|
||||||
"Authorization": window.localStorage.getItem('token'),
|
"Authorization": window.localStorage.getItem('token'),
|
||||||
"Content-Type": "application/json"
|
"Content-Type": "application/json"
|
||||||
|
|||||||
@@ -0,0 +1,129 @@
|
|||||||
|
<template>
|
||||||
|
<div class="unified-empty-state" :class="{ 'small': size === 'small', 'large': size === 'large' }">
|
||||||
|
<el-empty
|
||||||
|
:description="description"
|
||||||
|
:image-size="computedImageSize"
|
||||||
|
:image="image"
|
||||||
|
>
|
||||||
|
<template #description>
|
||||||
|
<p class="empty-description">{{ description }}</p>
|
||||||
|
</template>
|
||||||
|
<slot></slot>
|
||||||
|
</el-empty>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'EmptyState',
|
||||||
|
props: {
|
||||||
|
description: {
|
||||||
|
type: String,
|
||||||
|
default: '暂无数据'
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
type: String,
|
||||||
|
default: 'default', // 'small', 'default', 'large'
|
||||||
|
validator: value => ['small', 'default', 'large'].includes(value)
|
||||||
|
},
|
||||||
|
image: {
|
||||||
|
type: String,
|
||||||
|
default: undefined
|
||||||
|
},
|
||||||
|
imageSize: {
|
||||||
|
type: Number,
|
||||||
|
default: undefined
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
computedImageSize() {
|
||||||
|
if (this.imageSize) return this.imageSize;
|
||||||
|
|
||||||
|
switch (this.size) {
|
||||||
|
case 'small':
|
||||||
|
return 80;
|
||||||
|
case 'large':
|
||||||
|
return 120;
|
||||||
|
default:
|
||||||
|
return 100;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.unified-empty-state {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 200px;
|
||||||
|
padding: 20px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.unified-empty-state.small {
|
||||||
|
min-height: 150px;
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.unified-empty-state.large {
|
||||||
|
min-height: 250px;
|
||||||
|
padding: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-description {
|
||||||
|
color: #909399;
|
||||||
|
font-size: 14px;
|
||||||
|
margin: 0;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 针对网格布局的支持 */
|
||||||
|
.unified-empty-state {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式设计 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.unified-empty-state {
|
||||||
|
min-height: 150px;
|
||||||
|
padding: 15px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.unified-empty-state.small {
|
||||||
|
min-height: 120px;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.unified-empty-state.large {
|
||||||
|
min-height: 180px;
|
||||||
|
padding: 20px 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-description {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.unified-empty-state {
|
||||||
|
min-height: 120px;
|
||||||
|
padding: 10px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.unified-empty-state.small {
|
||||||
|
min-height: 100px;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.unified-empty-state.large {
|
||||||
|
min-height: 150px;
|
||||||
|
padding: 15px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-description {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -20,9 +20,10 @@
|
|||||||
<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" style="margin-top: 10px;">
|
<EmptyState
|
||||||
<el-empty description="暂无转发配置"></el-empty>
|
v-if="forwardList.length === 0 && !loading"
|
||||||
</div>
|
description="暂无转发配置"
|
||||||
|
/>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-for="forward in forwardList"
|
v-for="forward in forwardList"
|
||||||
@@ -324,9 +325,13 @@ import {
|
|||||||
resumeForwardService
|
resumeForwardService
|
||||||
} from "@/api";
|
} from "@/api";
|
||||||
import { copyWithMessage } from "@/utils/clipboard";
|
import { copyWithMessage } from "@/utils/clipboard";
|
||||||
|
import EmptyState from "@/components/EmptyState.vue";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "Forward",
|
name: "Forward",
|
||||||
|
components: {
|
||||||
|
EmptyState
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
loading: false,
|
loading: false,
|
||||||
@@ -1313,13 +1318,7 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty-state {
|
/* 空状态使用统一组件,样式已内置 */
|
||||||
grid-column: 1 / -1;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
height: 200px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flow-stats-mini {
|
.flow-stats-mini {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -1554,11 +1553,7 @@ export default {
|
|||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 移动端空状态调整 */
|
/* 移动端空状态样式已在统一组件中处理 */
|
||||||
.empty-state {
|
|
||||||
padding: 20px 15px;
|
|
||||||
height: 150px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 移动端form hint */
|
/* 移动端form hint */
|
||||||
.form-hint {
|
.form-hint {
|
||||||
|
|||||||
@@ -88,8 +88,12 @@
|
|||||||
<div class="tunnels-section">
|
<div class="tunnels-section">
|
||||||
<h3 class="section-title">隧道权限</h3>
|
<h3 class="section-title">隧道权限</h3>
|
||||||
<div v-loading="loading" class="tunnels-grid">
|
<div v-loading="loading" class="tunnels-grid">
|
||||||
<div v-if="userTunnels.length === 0 && !loading" class="empty-state">
|
<!-- 空状态也使用卡片容器,保持样式统一 -->
|
||||||
<el-empty description="暂无隧道权限"></el-empty>
|
<div v-if="userTunnels.length === 0 && !loading" class="tunnel-card empty-card">
|
||||||
|
<EmptyState
|
||||||
|
description="暂无隧道权限"
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@@ -177,9 +181,12 @@
|
|||||||
<div class="forwards-section">
|
<div class="forwards-section">
|
||||||
<h3 class="section-title">转发详情</h3>
|
<h3 class="section-title">转发详情</h3>
|
||||||
<div v-loading="loading" class="forwards-container">
|
<div v-loading="loading" class="forwards-container">
|
||||||
<div v-if="groupedForwards.length === 0 && !loading" class="empty-state">
|
<!-- 空状态保持在容器内,与有数据时样式统一 -->
|
||||||
<el-empty description="暂无转发配置"></el-empty>
|
<EmptyState
|
||||||
</div>
|
v-if="groupedForwards.length === 0 && !loading"
|
||||||
|
description="暂无转发配置"
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
|
||||||
<div v-for="group in groupedForwards" :key="group.tunnelName" class="forward-group">
|
<div v-for="group in groupedForwards" :key="group.tunnelName" class="forward-group">
|
||||||
<div class="group-header">
|
<div class="group-header">
|
||||||
@@ -267,9 +274,13 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { getUserPackageInfo } from "@/api";
|
import { getUserPackageInfo } from "@/api";
|
||||||
|
import EmptyState from "@/components/EmptyState.vue";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "Index",
|
name: "Index",
|
||||||
|
components: {
|
||||||
|
EmptyState
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
refreshLoading: false,
|
refreshLoading: false,
|
||||||
@@ -1071,14 +1082,19 @@ export default {
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty-state {
|
/* 隧道权限空状态卡片样式 */
|
||||||
|
.tunnel-card.empty-card {
|
||||||
grid-column: 1 / -1;
|
grid-column: 1 / -1;
|
||||||
|
justify-self: center;
|
||||||
|
max-width: 400px;
|
||||||
|
min-height: 160px;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
height: 200px;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 转发详情的空状态样式由EmptyState组件内置处理 */
|
||||||
|
|
||||||
/* 响应式设计 */
|
/* 响应式设计 */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.package-container {
|
.package-container {
|
||||||
@@ -1155,6 +1171,12 @@ export default {
|
|||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tunnel-card.empty-card {
|
||||||
|
max-width: 100%;
|
||||||
|
min-height: 140px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
.tunnel-header {
|
.tunnel-header {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
|
|||||||
@@ -143,11 +143,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 空状态 -->
|
<!-- 空状态 -->
|
||||||
<div v-if="!loading && filteredSpeedLimitList.length === 0" style="margin-top: 10px;">
|
<EmptyState
|
||||||
<el-empty description="暂无限速规则">
|
v-if="!loading && filteredSpeedLimitList.length === 0"
|
||||||
|
description="暂无限速规则"
|
||||||
</el-empty>
|
/>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 新增/编辑对话框 -->
|
<!-- 新增/编辑对话框 -->
|
||||||
<el-dialog
|
<el-dialog
|
||||||
@@ -220,9 +219,13 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { createSpeedLimit, getSpeedLimitList, updateSpeedLimit, deleteSpeedLimit, getTunnelList } from "@/api";
|
import { createSpeedLimit, getSpeedLimitList, updateSpeedLimit, deleteSpeedLimit, getTunnelList } from "@/api";
|
||||||
|
import EmptyState from "@/components/EmptyState.vue";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "Limit",
|
name: "Limit",
|
||||||
|
components: {
|
||||||
|
EmptyState
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
speedLimitList: [],
|
speedLimitList: [],
|
||||||
@@ -617,13 +620,7 @@ export default {
|
|||||||
flex: 1;
|
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 {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
|||||||
+919
-120
File diff suppressed because it is too large
Load Diff
@@ -103,9 +103,10 @@
|
|||||||
|
|
||||||
<!-- 移动端卡片视图 -->
|
<!-- 移动端卡片视图 -->
|
||||||
<div class="mobile-view" v-loading="loading">
|
<div class="mobile-view" v-loading="loading">
|
||||||
<div v-if="userList.length === 0 && !loading" class="empty-state">
|
<EmptyState
|
||||||
<el-empty description="暂无用户数据"></el-empty>
|
v-if="userList.length === 0 && !loading"
|
||||||
</div>
|
description="暂无用户数据"
|
||||||
|
/>
|
||||||
|
|
||||||
<div v-for="user in userList" :key="user.id" class="user-card">
|
<div v-for="user in userList" :key="user.id" class="user-card">
|
||||||
<!-- 卡片头部 -->
|
<!-- 卡片头部 -->
|
||||||
@@ -532,9 +533,10 @@
|
|||||||
|
|
||||||
<!-- 移动端卡片视图 -->
|
<!-- 移动端卡片视图 -->
|
||||||
<div class="mobile-view" v-loading="tunnelListLoading">
|
<div class="mobile-view" v-loading="tunnelListLoading">
|
||||||
<div v-if="userTunnelList.length === 0 && !tunnelListLoading" class="empty-state">
|
<EmptyState
|
||||||
<el-empty description="暂无隧道权限" :image-size="100"></el-empty>
|
v-if="userTunnelList.length === 0 && !tunnelListLoading"
|
||||||
</div>
|
description="暂无隧道权限"
|
||||||
|
/>
|
||||||
|
|
||||||
<div v-for="tunnel in userTunnelList" :key="tunnel.id" class="tunnel-card">
|
<div v-for="tunnel in userTunnelList" :key="tunnel.id" class="tunnel-card">
|
||||||
<!-- 卡片头部 -->
|
<!-- 卡片头部 -->
|
||||||
@@ -717,9 +719,13 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { getAllUsers, createUser, updateUser, deleteUser, getTunnelList, assignUserTunnel, getUserTunnelList, removeUserTunnel, updateUserTunnelFlow, updateUserTunnel, getSpeedLimitList } from "@/api";
|
import { getAllUsers, createUser, updateUser, deleteUser, getTunnelList, assignUserTunnel, getUserTunnelList, removeUserTunnel, updateUserTunnelFlow, updateUserTunnel, getSpeedLimitList } from "@/api";
|
||||||
|
import EmptyState from "@/components/EmptyState.vue";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "User",
|
name: "User",
|
||||||
|
components: {
|
||||||
|
EmptyState
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
userList: [],
|
userList: [],
|
||||||
|
|||||||
@@ -144,14 +144,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 空状态 -->
|
<!-- 空状态 -->
|
||||||
<div v-if="!loading && nodeList.length === 0" class="empty-state">
|
<EmptyState
|
||||||
<i class="el-icon-box"></i>
|
v-if="!loading && nodeList.length === 0"
|
||||||
<p>暂无节点数据</p>
|
description="暂无节点数据"
|
||||||
<el-button type="primary" size="small" @click="handleAdd">
|
/>
|
||||||
<i class="el-icon-plus"></i>
|
|
||||||
新增第一个节点
|
|
||||||
</el-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 对话框保持不变 -->
|
<!-- 对话框保持不变 -->
|
||||||
@@ -315,6 +311,7 @@
|
|||||||
import {createNode, updateNode, deleteNode, getNodeList, getNodeInstallCommand} from "@/api";
|
import {createNode, updateNode, deleteNode, getNodeList, getNodeInstallCommand} from "@/api";
|
||||||
import { copyWithMessage, copyWithFallback } from "@/utils/clipboard";
|
import { copyWithMessage, copyWithFallback } from "@/utils/clipboard";
|
||||||
import VChart from 'vue-echarts';
|
import VChart from 'vue-echarts';
|
||||||
|
import EmptyState from "@/components/EmptyState.vue";
|
||||||
import { use } from 'echarts/core';
|
import { use } from 'echarts/core';
|
||||||
import { CanvasRenderer } from 'echarts/renderers';
|
import { CanvasRenderer } from 'echarts/renderers';
|
||||||
import { GaugeChart } from 'echarts/charts';
|
import { GaugeChart } from 'echarts/charts';
|
||||||
@@ -330,7 +327,8 @@ use([
|
|||||||
export default {
|
export default {
|
||||||
name: "Node",
|
name: "Node",
|
||||||
components: {
|
components: {
|
||||||
VChart
|
VChart,
|
||||||
|
EmptyState
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@@ -1285,23 +1283,7 @@ export default {
|
|||||||
padding-top: 12px;
|
padding-top: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 空状态 */
|
/* 空状态使用统一组件,样式已内置 */
|
||||||
.empty-state {
|
|
||||||
text-align: center;
|
|
||||||
padding: 40px 20px;
|
|
||||||
color: #909399;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-state i {
|
|
||||||
font-size: 48px;
|
|
||||||
margin-bottom: 15px;
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-state p {
|
|
||||||
font-size: 14px;
|
|
||||||
margin-bottom: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 响应式设计 */
|
/* 响应式设计 */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
|
|||||||
Reference in New Issue
Block a user