mirror of
https://github.com/nezhahq/nezha.git
synced 2025-01-22 20:58:14 -05:00
server-status主题重写network页面 (#407)
* 修复浏览器高分辨率下网络页echart图表显示不全 * server-status主题重写network页面 1. 点击“网络监控”弹出选择服务器 2. 增加服务器搜索筛选功能 3. 自适应lenged高度 4. 适配深色模式与浅色模式 5. 适配半透明模式 6. 适配pc段与移动端 * 刷新cdn缓存 * 增加disposeCharts逻辑 * 增加disposeCharts逻辑 * 标题增加服务器所在地区国旗展示 * 国旗样式修复 * 修复国旗样式 * echart图标高度适配不同分辨率浏览器
This commit is contained in:
parent
d9097540c3
commit
c18e0e420e
3
resource/l10n/en-US.toml
vendored
3
resource/l10n/en-US.toml
vendored
@ -651,3 +651,6 @@ other = "Servers On World Map"
|
|||||||
|
|
||||||
[NAT]
|
[NAT]
|
||||||
other = "NAT"
|
other = "NAT"
|
||||||
|
|
||||||
|
[NetworkSpiterList]
|
||||||
|
other = "Network Monitor"
|
3
resource/l10n/es-ES.toml
vendored
3
resource/l10n/es-ES.toml
vendored
@ -651,3 +651,6 @@ other = "Servidores en el mapa mundial"
|
|||||||
|
|
||||||
[NAT]
|
[NAT]
|
||||||
other = "NAT"
|
other = "NAT"
|
||||||
|
|
||||||
|
[NetworkSpiterList]
|
||||||
|
other = "Red Monitor"
|
3
resource/l10n/zh-CN.toml
vendored
3
resource/l10n/zh-CN.toml
vendored
@ -651,3 +651,6 @@ other = "服务器世界分布图"
|
|||||||
|
|
||||||
[NAT]
|
[NAT]
|
||||||
other = "内网穿透"
|
other = "内网穿透"
|
||||||
|
|
||||||
|
[NetworkSpiterList]
|
||||||
|
other = "网络监控"
|
||||||
|
3
resource/l10n/zh-TW.toml
vendored
3
resource/l10n/zh-TW.toml
vendored
@ -651,3 +651,6 @@ other = "伺服器世界分布圖"
|
|||||||
|
|
||||||
[NAT]
|
[NAT]
|
||||||
other = "NAT"
|
other = "NAT"
|
||||||
|
|
||||||
|
[NetworkSpiterList]
|
||||||
|
other = "網絡監控"
|
@ -172,3 +172,7 @@ body[theme="dark"] .toolbox i{
|
|||||||
color: rgba(241, 241, 241, 1);
|
color: rgba(241, 241, 241, 1);
|
||||||
background-color: rgba(0, 0, 0, 0.5);
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body[theme="dark"] .network-box .network-box-header{
|
||||||
|
border-bottom: 1px solid rgba(110, 112, 121, 0.25);
|
||||||
|
}
|
@ -197,6 +197,10 @@ body[theme="light"] .modal-header i.xclose{
|
|||||||
color: rgba(0, 0, 0, 0.87);
|
color: rgba(0, 0, 0, 0.87);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body[theme="light"] .network-box .network-box-header{
|
||||||
|
border-bottom: 1px solid rgba(224, 230, 241, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: 767px) {
|
@media only screen and (max-width: 767px) {
|
||||||
body[theme="light"] .navbar .navbar-nav .open .dropdown-menu {
|
body[theme="light"] .navbar .navbar-nav .open .dropdown-menu {
|
||||||
background-color: rgba(249, 249, 249, 1);
|
background-color: rgba(249, 249, 249, 1);
|
||||||
|
90
resource/static/theme-server-status/css/main.css
vendored
90
resource/static/theme-server-status/css/main.css
vendored
@ -317,6 +317,78 @@ td.ping-network-quality {
|
|||||||
}
|
}
|
||||||
/* 服务页 正文结束 */
|
/* 服务页 正文结束 */
|
||||||
|
|
||||||
|
/* 网络页 正文*/
|
||||||
|
.network-box .btn-group.open .dropdown-toggle{
|
||||||
|
box-shadow:unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.network-box .network-box-header{
|
||||||
|
font-size: 18px;
|
||||||
|
padding: 5px 0px 15px 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.network-box .network-box-header .dropdown-menu {
|
||||||
|
max-height: 22.5em;
|
||||||
|
overflow-y: auto;
|
||||||
|
z-index:99999998;
|
||||||
|
min-width: 200px;
|
||||||
|
padding: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.network-box .network-box-header .dropdown-menu::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.network-box .network-box-header .dropdown-menu li{
|
||||||
|
height: 2em;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.network-box .network-box-header .dropdown-menu li.input-group{
|
||||||
|
margin: 0 auto;
|
||||||
|
display: flex;
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 99999997;
|
||||||
|
}
|
||||||
|
|
||||||
|
.network-box .network-box-header .dropdown-menu li.input-group input{
|
||||||
|
width: 100%;
|
||||||
|
height: 2em;
|
||||||
|
border-top: none;
|
||||||
|
border-left: none;
|
||||||
|
border-right: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.network-box .network-box-header .dropdown-menu li a {
|
||||||
|
padding: 5px 5px 5px 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.network-box .network-box-header .dropdown-menu li.hidden {
|
||||||
|
height: 0;
|
||||||
|
display: none;
|
||||||
|
visibility:hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.network-box .network-box-header .dropdown-menu li.visible {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.network-box .chartTitle {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 18px;
|
||||||
|
margin: 18px 0px 15px 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.network-box .chartTitle i.chartCountryCode{
|
||||||
|
font-size: 16px;
|
||||||
|
border-radius: 12.5%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 网络页 正文结束 */
|
||||||
|
|
||||||
/* 地图版服务器分布图 */
|
/* 地图版服务器分布图 */
|
||||||
#mapChartBox{
|
#mapChartBox{
|
||||||
z-index: 999999999;
|
z-index: 999999999;
|
||||||
@ -393,6 +465,12 @@ footer p{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 彩虹旗 */
|
||||||
|
.fi-rb {
|
||||||
|
background-image: url(/static/theme-server-status/img/rb.png);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@media only screen and (max-width: 767px) {
|
@media only screen and (max-width: 767px) {
|
||||||
body {
|
body {
|
||||||
font-size: 10px !important;
|
font-size: 10px !important;
|
||||||
@ -482,6 +560,18 @@ footer p{
|
|||||||
td.ping-network-quality {
|
td.ping-network-quality {
|
||||||
width: 110px;
|
width: 110px;
|
||||||
}
|
}
|
||||||
|
.network-box .network-box-header {
|
||||||
|
margin: 8px 0px 0px 8px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
.network-box .chartTitle {
|
||||||
|
font-size: 16px;
|
||||||
|
margin: 10px 0px 10px 0px;
|
||||||
|
}
|
||||||
|
.network-box .chartTitle i.chartCountryCode{
|
||||||
|
font-size: 15px;
|
||||||
|
border-radius: 12.5%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (min-width: 768px) {
|
@media only screen and (min-width: 768px) {
|
||||||
|
BIN
resource/static/theme-server-status/img/rb.png
vendored
Normal file
BIN
resource/static/theme-server-status/img/rb.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.8 KiB |
10
resource/static/theme-server-status/js/mixin.js
vendored
10
resource/static/theme-server-status/js/mixin.js
vendored
@ -14,7 +14,10 @@ const mixinsVue = {
|
|||||||
{ key: 'default', name: 'Default', icon: 'th large' },
|
{ key: 'default', name: 'Default', icon: 'th large' },
|
||||||
{ key: 'angel-kanade', name: 'AngelKanade', icon: 'square' },
|
{ key: 'angel-kanade', name: 'AngelKanade', icon: 'square' },
|
||||||
{ key: 'server-status', name: 'ServerStatus', icon: 'list' }
|
{ key: 'server-status', name: 'ServerStatus', icon: 'list' }
|
||||||
]
|
],
|
||||||
|
colors: [],
|
||||||
|
colorsDark: ['#4992FF', '#08C091', '#FDDD5F', '#FF6E76', '#58D9F9', '#7CFFB2', '#FF8A44', '#8D48E3', '#DD79FF', '#5470C6', '#3BA272', '#FAC758', '#EE6666', '#72C0DE', '#91CC76', '#FB8352', '#9A60B4', '#EA7BCC'],
|
||||||
|
colorsLight: ['#5470C6', '#3BA272', '#FAC758', '#EE6666', '#72C0DE', '#91CC76', '#FB8352', '#9A60B4', '#EA7BCC', '#4992FF', '#08C091', '#FDDD5F', '#FF6E76', '#58D9F9', '#7CFFB2', '#FF8A44', '#8D48E3', '#DD79FF'],
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
this.isMobile = this.checkIsMobile();
|
this.isMobile = this.checkIsMobile();
|
||||||
@ -22,6 +25,7 @@ const mixinsVue = {
|
|||||||
this.showGroup = this.initShowGroup();
|
this.showGroup = this.initShowGroup();
|
||||||
this.semiTransparent = this.initSemiTransparent();
|
this.semiTransparent = this.initSemiTransparent();
|
||||||
this.preferredTemplate = this.getCookie('preferred_theme') ? this.getCookie('preferred_theme') : this.$root.defaultTemplate;
|
this.preferredTemplate = this.getCookie('preferred_theme') ? this.getCookie('preferred_theme') : this.$root.defaultTemplate;
|
||||||
|
this.colors = this.theme == "dark" ? this.colorsDark : this.colorsLight;
|
||||||
window.addEventListener('scroll', this.handleScroll);
|
window.addEventListener('scroll', this.handleScroll);
|
||||||
},
|
},
|
||||||
destroyed() {
|
destroyed() {
|
||||||
@ -51,7 +55,7 @@ const mixinsVue = {
|
|||||||
// 重新赋值全局调色
|
// 重新赋值全局调色
|
||||||
this.colors = this.theme == "dark" ? this.colorsDark : this.colorsLight;
|
this.colors = this.theme == "dark" ? this.colorsDark : this.colorsLight;
|
||||||
|
|
||||||
if(this.$root.page == 'index') {
|
if(this.$root.page == 'index' || this.$root.page == 'network') {
|
||||||
this.reloadCharts(); // 重新载入echarts图表
|
this.reloadCharts(); // 重新载入echarts图表
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -81,7 +85,7 @@ const mixinsVue = {
|
|||||||
toggleSemiTransparent(){
|
toggleSemiTransparent(){
|
||||||
this.semiTransparent = !this.semiTransparent;
|
this.semiTransparent = !this.semiTransparent;
|
||||||
localStorage.setItem("semiTransparent", this.semiTransparent);
|
localStorage.setItem("semiTransparent", this.semiTransparent);
|
||||||
if(this.$root.page == 'index') {
|
if(this.$root.page == 'index' || this.$root.page == 'network') {
|
||||||
this.reloadCharts(); // 重新载入echarts图表
|
this.reloadCharts(); // 重新载入echarts图表
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -16,10 +16,10 @@
|
|||||||
<script src="https://unpkg.com/bootstrap@3.4.1/dist/js/bootstrap.min.js"></script>
|
<script src="https://unpkg.com/bootstrap@3.4.1/dist/js/bootstrap.min.js"></script>
|
||||||
<script src="https://unpkg.com/vue@2.6.14/dist/vue.min.js"></script>
|
<script src="https://unpkg.com/vue@2.6.14/dist/vue.min.js"></script>
|
||||||
<script src="https://unpkg.com/echarts@5.5.0/dist/echarts.min.js"></script>
|
<script src="https://unpkg.com/echarts@5.5.0/dist/echarts.min.js"></script>
|
||||||
<link rel="stylesheet" href="/static/theme-server-status/css/main.css?v20240807">
|
<link rel="stylesheet" href="/static/theme-server-status/css/main.css?v202408011">
|
||||||
<link rel="stylesheet" href="/static/theme-server-status/css/dark.css?v20240807">
|
<link rel="stylesheet" href="/static/theme-server-status/css/dark.css?v202408011">
|
||||||
<link rel="stylesheet" href="/static/theme-server-status/css/light.css?v20240807">
|
<link rel="stylesheet" href="/static/theme-server-status/css/light.css?v20240811">
|
||||||
<script src="/static/theme-server-status/js/mixin.js?v20240807"></script>
|
<script src="/static/theme-server-status/js/mixin.js?v20240811"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app">
|
<div id="app">
|
||||||
|
28
resource/template/theme-server-status/home.html
vendored
28
resource/template/theme-server-status/home.html
vendored
@ -155,14 +155,13 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const unit = this.language=='zh-CN' ? '台' : 'servers';
|
const unit = this.language=='zh-CN' ? '台' : 'servers';
|
||||||
const isMobile = this.checkIsMobile();
|
|
||||||
const width = window.innerWidth;
|
const width = window.innerWidth;
|
||||||
const height = 0.95 * window.innerHeight;
|
const height = 0.95 * window.innerHeight;
|
||||||
const backgroundColor = this.theme == "dark" ? '' : '';
|
const backgroundColor = this.theme == "dark" ? '' : '';
|
||||||
const inRangeColor = this.theme == "dark" ? '#D2B206' : '#FFDF32';
|
const inRangeColor = this.theme == "dark" ? '#D2B206' : '#FFDF32';
|
||||||
const tooltipBackgroundColor = this.theme == "dark" ? "#ffffff" : '#ffffff';
|
const tooltipBackgroundColor = this.theme == "dark" ? "#ffffff" : '#ffffff';
|
||||||
const tooltipBorderColor = this.theme == "dark" ? "#ffffff" : "#ffffff";
|
const tooltipBorderColor = this.theme == "dark" ? "#ffffff" : "#ffffff";
|
||||||
const fontSize = isMobile ? 10 : 12;
|
const fontSize = this.isMobile ? 10 : 12;
|
||||||
const fontColor = this.theme == "dark" ? "#000000" : "#000000";
|
const fontColor = this.theme == "dark" ? "#000000" : "#000000";
|
||||||
const chartContainer = document.getElementById('mapChart');
|
const chartContainer = document.getElementById('mapChart');
|
||||||
const mapChart = echarts.init(chartContainer, '', { // init图表
|
const mapChart = echarts.init(chartContainer, '', { // init图表
|
||||||
@ -510,13 +509,12 @@
|
|||||||
}
|
}
|
||||||
// 定义图表参数值
|
// 定义图表参数值
|
||||||
const MaxTCPPingValue = {{.Conf.MaxTCPPingValue}} ? {{.Conf.MaxTCPPingValue}} : 300;
|
const MaxTCPPingValue = {{.Conf.MaxTCPPingValue}} ? {{.Conf.MaxTCPPingValue}} : 300;
|
||||||
const isMobile = this.checkIsMobile();
|
const fontSize = this.isMobile ? 10 : 14;
|
||||||
const fontSize = isMobile ? 10 : 14;
|
const gridLeft = this.isMobile ? 25 : 36;
|
||||||
const gridLeft = isMobile ? 25 : 36;
|
const gridRight = this.isMobile ? 5 : 20;
|
||||||
const gridRight = isMobile ? 5 : 20;
|
const legendLeft = this.isMobile ? 'center' : 'center';
|
||||||
const legendLeft = isMobile ? 'center' : 'center';
|
const legendTop = this.isMobile ? 5 : 5;
|
||||||
const legendTop = isMobile ? 5 : 5;
|
const legendPadding= this.isMobile ? [5,0,5,0] : [5,0,5,0];
|
||||||
const legendPadding= isMobile ? [5,0,5,0] : [5,0,5,0];
|
|
||||||
const systemDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
const systemDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
||||||
const theme = localStorage.getItem("theme") ? localStorage.getItem("theme") : systemDarkMode;
|
const theme = localStorage.getItem("theme") ? localStorage.getItem("theme") : systemDarkMode;
|
||||||
const chartTheme = theme == "dark" ? "dark" : "default";
|
const chartTheme = theme == "dark" ? "dark" : "default";
|
||||||
@ -524,8 +522,8 @@
|
|||||||
const backgroundColor = theme == "dark" ? '' : '';
|
const backgroundColor = theme == "dark" ? '' : '';
|
||||||
const tooltipBackgroundColor = this.theme == "dark" ? (this.semiTransparent ? "rgba(28,29,38,0.85)" : "rgba(28,29,38,1)") : (this.semiTransparent ? "rgba(255,255,255,0.85)" : "rgba(255,255,255,1)");
|
const tooltipBackgroundColor = this.theme == "dark" ? (this.semiTransparent ? "rgba(28,29,38,0.85)" : "rgba(28,29,38,1)") : (this.semiTransparent ? "rgba(255,255,255,0.85)" : "rgba(255,255,255,1)");
|
||||||
const tooltipBorderColor = this.theme == "dark" ? (this.semiTransparent ? "rgba(28,29,38,0.9)" : "rgba(28,29,38,1)") : (this.semiTransparent ? "rgba(255,255,255,0.9)" : "rgba(255,255,255,1)");
|
const tooltipBorderColor = this.theme == "dark" ? (this.semiTransparent ? "rgba(28,29,38,0.9)" : "rgba(28,29,38,1)") : (this.semiTransparent ? "rgba(255,255,255,0.9)" : "rgba(255,255,255,1)");
|
||||||
const lineStyleWidth = isMobile ? 1 : 2;
|
const lineStyleWidth = this.isMobile ? 1 : 2;
|
||||||
const splitLineWidth = isMobile ? 0.5 : 1;
|
const splitLineWidth = this.isMobile ? 0.5 : 1;
|
||||||
// 渲染图表
|
// 渲染图表
|
||||||
const chart = echarts.init(chartContainer, chartTheme, {
|
const chart = echarts.init(chartContainer, chartTheme, {
|
||||||
renderer: 'canvas',
|
renderer: 'canvas',
|
||||||
@ -562,12 +560,12 @@
|
|||||||
const legendData = chartData.map(item => item.monitor_name);
|
const legendData = chartData.map(item => item.monitor_name);
|
||||||
const maxLegendsPerRowMobile = localStorage.getItem("maxLegendsPerRowMobile") ? localStorage.getItem("maxLegendsPerRowMobile") : 3;
|
const maxLegendsPerRowMobile = localStorage.getItem("maxLegendsPerRowMobile") ? localStorage.getItem("maxLegendsPerRowMobile") : 3;
|
||||||
const maxLegendsPerRowPc = localStorage.getItem("maxLegendsPerRowPc") ? localStorage.getItem("maxLegendsPerRowPc") : 6;
|
const maxLegendsPerRowPc = localStorage.getItem("maxLegendsPerRowPc") ? localStorage.getItem("maxLegendsPerRowPc") : 6;
|
||||||
const autoIncrement = Math.floor((legendData.length - 1) / (isMobile ? maxLegendsPerRowMobile : maxLegendsPerRowPc)) * (isMobile ? 20 : 28)
|
const autoIncrement = Math.floor((legendData.length - 1) / (this.isMobile ? maxLegendsPerRowMobile : maxLegendsPerRowPc)) * (this.isMobile ? 20 : 28)
|
||||||
const height = 300 + autoIncrement;
|
const height = 300 + autoIncrement;
|
||||||
const gridTop = 40 + autoIncrement;
|
const gridTop = 40 + autoIncrement;
|
||||||
const legendIcon = isMobile ? 'rect' : "";
|
const legendIcon = this.isMobile ? 'rect' : "";
|
||||||
const itemWidth = isMobile ? 10 : 25;
|
const itemWidth = this.isMobile ? 10 : 25;
|
||||||
const itemHeight = isMobile ? 10 : 14;
|
const itemHeight = this.isMobile ? 10 : 14;
|
||||||
chart.resize({
|
chart.resize({
|
||||||
width: 'auto',
|
width: 'auto',
|
||||||
height: height
|
height: height
|
||||||
|
521
resource/template/theme-server-status/network.html
vendored
521
resource/template/theme-server-status/network.html
vendored
@ -1,239 +1,362 @@
|
|||||||
{{define "theme-server-status/network"}}
|
{{define "theme-server-status/network"}}
|
||||||
{{template "theme-server-status/header" .}}
|
{{template "theme-server-status/header" .}}
|
||||||
{{template "theme-server-status/menu" .}}
|
{{template "theme-server-status/menu" .}}
|
||||||
<div class="container-fluid table-responsive content">
|
<div class="container-fluid content network-box">
|
||||||
<table class="table table-striped table-condensed table-hover">
|
<div class="network-box-header btn-group">
|
||||||
<button class="ui nezha-primary-btn button"
|
<div class="dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||||
v-for="server in servers"
|
<i class="bi bi-list-ul"></i> {{tr "NetworkSpiterList"}} <i class="bi bi-chevron-compact-down"></i>
|
||||||
style="margin-top: 3px"
|
</div>
|
||||||
@click="redirectNetwork(server.ID)">
|
<ul class="dropdown-menu">
|
||||||
@#server.Name#@ <i :class="'fi fi-' + server.Host.CountryCode"></i><span class="node-cell-location-text text-uppercase"> @#server.Host.CountryCode#@</span>
|
<li class="input-group fixed-top">
|
||||||
</button>
|
<input type="text" id="dropdown-search" class="form-control" placeholder="Search...">
|
||||||
</table>
|
</li>
|
||||||
</div>
|
<li class="dropdown-item" v-for="server in servers" @click="showCharts(server.ID)">
|
||||||
<div class="container-fluid table-responsive content">
|
<a><i :class="'fi fi-' + (server.Host.CountryCode || 'rb')"></i> @#server.Name#@ <i v-if="server.ID == currentServerId" class="check icon"></i></a>
|
||||||
<div ref="chartDom" style="border-radius: 28px; margin-top: 15px;height: 520px;max-width: 1400px;overflow: hidden"></div>
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="chartTitle"><i class="chartCountryCode" :class="'fi fi-' + chartCountryCode"></i> @#chartTitle#@</div>
|
||||||
|
<div id="chartbox" style="width:100%;height:auto;"></div>
|
||||||
</div>
|
</div>
|
||||||
{{template "theme-server-status/footer" .}}
|
{{template "theme-server-status/footer" .}}
|
||||||
<script>
|
<script>
|
||||||
const monitorInfo = JSON.parse('{{.MonitorInfos}}');
|
|
||||||
const initData = JSON.parse('{{.Servers}}').servers;
|
|
||||||
let MaxTCPPingValue = {{.Conf.MaxTCPPingValue}};
|
|
||||||
new Vue({
|
new Vue({
|
||||||
el: '#app',
|
el: '#app',
|
||||||
delimiters: ['@#', '#@'],
|
delimiters: ['@#', '#@'],
|
||||||
data: {
|
data: {
|
||||||
page: 'network',
|
page: 'network',
|
||||||
defaultTemplate: {{.Conf.Site.Theme}},
|
defaultTemplate: "{{.Conf.Site.Theme}}",
|
||||||
templates: {{.Themes}},
|
templates: "{{.Themes }}",
|
||||||
servers: initData,
|
servers: [],
|
||||||
option: {
|
chartDataList: [],
|
||||||
tooltip: {
|
chartTitle: '',
|
||||||
trigger: 'axis',
|
chartCountryCode: '',
|
||||||
position: function (pt) {
|
chart: null,
|
||||||
return [pt[0], '10%'];
|
currentServerId: ''
|
||||||
},
|
|
||||||
formatter: function(params){
|
|
||||||
let result = params[0].axisValueLabel + "<br />";
|
|
||||||
params.forEach(function(item){
|
|
||||||
result += item.marker + item.seriesName + ": " + item.value[1].toFixed(2) + " ms<br />";
|
|
||||||
})
|
|
||||||
return result;
|
|
||||||
},
|
|
||||||
confine: true,
|
|
||||||
transitionDuration: 0
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
left: 'center',
|
|
||||||
text: "",
|
|
||||||
textStyle: {}
|
|
||||||
},
|
|
||||||
legend: {
|
|
||||||
top: '5%',
|
|
||||||
data: [],
|
|
||||||
textStyle: {
|
|
||||||
fontSize: 14
|
|
||||||
}
|
|
||||||
},
|
|
||||||
backgroundColor: 'rgba(255, 255, 255, 0.8)',
|
|
||||||
toolbox: {
|
|
||||||
feature: {
|
|
||||||
dataZoom: {
|
|
||||||
yAxisIndex: 'none'
|
|
||||||
},
|
|
||||||
restore: {},
|
|
||||||
saveAsImage: {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
dataZoom: [
|
|
||||||
{
|
|
||||||
start: 0,
|
|
||||||
end: 100
|
|
||||||
}
|
|
||||||
],
|
|
||||||
xAxis: {
|
|
||||||
type: 'time',
|
|
||||||
boundaryGap: false
|
|
||||||
},
|
|
||||||
yAxis: {
|
|
||||||
type: 'value',
|
|
||||||
boundaryGap: false
|
|
||||||
},
|
|
||||||
series: [],
|
|
||||||
},
|
|
||||||
chartOnOff: true,
|
|
||||||
},
|
},
|
||||||
mixins: [mixinsVue],
|
mixins: [mixinsVue],
|
||||||
|
created() {
|
||||||
|
this.servers = JSON.parse('{{.Servers}}').servers;
|
||||||
|
this.showCharts(this.servers[0].ID);
|
||||||
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.renderChart();
|
this.initSearch();
|
||||||
this.parseMonitorInfo(monitorInfo);
|
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
getFontLogoClass(str) {
|
showCharts(id) {
|
||||||
if (["almalinux",
|
const chartContainer = document.getElementById('chartbox');
|
||||||
"alpine",
|
// 发起数据请求
|
||||||
"aosc",
|
const url = `/api/v1/monitor/${id}`;
|
||||||
"apple",
|
fetch(url)
|
||||||
"archlinux",
|
.then(response => response.json())
|
||||||
"archlabs",
|
.then(data => {
|
||||||
"artix",
|
if (data.result) { // 数据请求成功,更新数据并渲染图表
|
||||||
"budgie",
|
this.chartDataList[id] = data.result;
|
||||||
"centos",
|
this.$nextTick(() => {
|
||||||
"coreos",
|
this.renderCharts(id);
|
||||||
"debian",
|
});
|
||||||
"deepin",
|
} else {
|
||||||
"devuan",
|
console.log('this server (id:'+ id + ') has no monitor.');
|
||||||
"docker",
|
|
||||||
"elementary",
|
|
||||||
"fedora",
|
|
||||||
"ferris",
|
|
||||||
"flathub",
|
|
||||||
"freebsd",
|
|
||||||
"gentoo",
|
|
||||||
"gnu-guix",
|
|
||||||
"illumos",
|
|
||||||
"kali-linux",
|
|
||||||
"linuxmint",
|
|
||||||
"mageia",
|
|
||||||
"mandriva",
|
|
||||||
"manjaro",
|
|
||||||
"nixos",
|
|
||||||
"openbsd",
|
|
||||||
"opensuse",
|
|
||||||
"pop-os",
|
|
||||||
"raspberry-pi",
|
|
||||||
"redhat",
|
|
||||||
"rocky-linux",
|
|
||||||
"sabayon",
|
|
||||||
"slackware",
|
|
||||||
"snappy",
|
|
||||||
"solus",
|
|
||||||
"tux",
|
|
||||||
"ubuntu",
|
|
||||||
"void",
|
|
||||||
"zorin"].indexOf(str)
|
|
||||||
> -1) {
|
|
||||||
return str;
|
|
||||||
}
|
|
||||||
if (['openwrt', 'linux', "immortalwrt"].indexOf(str) > -1) {
|
|
||||||
return 'tux';
|
|
||||||
}
|
|
||||||
if (str == 'amazon') {
|
|
||||||
return 'redhat';
|
|
||||||
}
|
|
||||||
if (str == 'arch') {
|
|
||||||
return 'archlinux';
|
|
||||||
}
|
|
||||||
return '';
|
|
||||||
},
|
|
||||||
redirectNetwork(id) {
|
|
||||||
this.getMonitorHistory(id)
|
|
||||||
.then(function(monitorInfo) {
|
|
||||||
var vm = app.__vue__;
|
|
||||||
vm.parseMonitorInfo(monitorInfo);
|
|
||||||
})
|
|
||||||
.catch(function(error){
|
|
||||||
window.location.href = "/404";
|
|
||||||
})
|
|
||||||
},
|
|
||||||
getMonitorHistory(id) {
|
|
||||||
return $.ajax({
|
|
||||||
url: "/api/v1/monitor/"+id,
|
|
||||||
method: "GET"
|
|
||||||
});
|
|
||||||
},
|
|
||||||
parseMonitorInfo(monitorInfo) {
|
|
||||||
let tSeries = [];
|
|
||||||
let tLegendData = [];
|
|
||||||
var lcolors = ['#5470c6', '#91cc75', '#fac858', '#ee6666', '#73c0de', '#3ba272', '#fc8452', '#9a60b4', '#ea7ccc'];
|
|
||||||
for (let i = 0; i < monitorInfo.result.length; i++) {
|
|
||||||
var lcolor = lcolors[i % lcolors.length];
|
|
||||||
var rgbaColorMarker = 'rgba(' + parseInt(lcolor.slice(1, 3), 16) + ',' + parseInt(lcolor.slice(3, 5), 16) + ',' + parseInt(lcolor.slice(5, 7), 16) + ',0.5)';
|
|
||||||
var rgbaColorBar = 'rgba(' + parseInt(lcolor.slice(1, 3), 16) + ',' + parseInt(lcolor.slice(3, 5), 16) + ',' + parseInt(lcolor.slice(5, 7), 16) + ',0.35)';
|
|
||||||
let loss = 0;
|
|
||||||
let data = [];
|
|
||||||
let datal = [];
|
|
||||||
for (let j = 0; j < monitorInfo.result[i].created_at.length; j++) {
|
|
||||||
avgDelay = Math.round(monitorInfo.result[i].avg_delay[j]);
|
|
||||||
if (avgDelay > 0 && avgDelay < MaxTCPPingValue) {
|
|
||||||
data.push([monitorInfo.result[i].created_at[j], avgDelay]);
|
|
||||||
}
|
}
|
||||||
else {
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error fetching data:', error);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
renderCharts(id, reload = false) {
|
||||||
|
if (!this.chartDataList[id]) return;
|
||||||
|
this.disposeCharts();
|
||||||
|
this.currentServerId = id;
|
||||||
|
this.chartCountryCode = this.getServerCountryCode(id);
|
||||||
|
const chartData = this.chartDataList[id];
|
||||||
|
const chartContainer = document.getElementById('chartbox');
|
||||||
|
this.chartTitle = chartData[0].server_name;
|
||||||
|
if (reload) {
|
||||||
|
const existingChart = echarts.getInstanceByDom(chartContainer);
|
||||||
|
if (existingChart) existingChart.dispose();
|
||||||
|
}
|
||||||
|
// 定义图表参数值
|
||||||
|
const MaxTCPPingValue = {{.Conf.MaxTCPPingValue}} ? {{.Conf.MaxTCPPingValue}} : 300;
|
||||||
|
const autoheight = this.isMobile ? (window.innerHeight - 200) : (window.innerHeight - 250);
|
||||||
|
const fontSize = this.isMobile ? 10 : 14;
|
||||||
|
const gridLeft = this.isMobile ? 30 : 36;
|
||||||
|
const gridRight = this.isMobile ? 12 : 20;
|
||||||
|
const legendLeft = this.isMobile ? 'center' : 'center';
|
||||||
|
const legendTop = this.isMobile ? 5 : 5;
|
||||||
|
const legendPadding= this.isMobile ? [5,0,5,0] : [5,0,5,0];
|
||||||
|
const fontColor = this.theme == "dark" ? "#f1f1f1" : "#000000";
|
||||||
|
const backgroundColor = this.theme == "dark" ? '' : '';
|
||||||
|
const tooltipBackgroundColor = this.theme == "dark" ? (this.semiTransparent ? "rgba(28,29,38,0.85)" : "rgba(28,29,38,1)") : (this.semiTransparent ? "rgba(255,255,255,0.85)" : "rgba(255,255,255,1)");
|
||||||
|
const tooltipBorderColor = this.theme == "dark" ? (this.semiTransparent ? "rgba(28,29,38,0.9)" : "rgba(28,29,38,1)") : (this.semiTransparent ? "rgba(255,255,255,0.9)" : "rgba(255,255,255,1)");
|
||||||
|
const lineStyleWidth = this.isMobile ? 1 : 2;
|
||||||
|
const splitLineWidth = this.isMobile ? 0.5 : 1;
|
||||||
|
const splitLineColor = this.theme == "dark" ? "rgba(110, 112, 121, 0.95)" : "rgba(224, 230, 241, 0.95)";
|
||||||
|
const markPointLabelColor = this.theme == "dark" ? "#111111" : "#000000";
|
||||||
|
const markPointItemStyleOpacity = this.semiTransparent ? 1 : 0.75;
|
||||||
|
const markLineItemStyleOpacity = this.semiTransparent ? 1 : 0.75;
|
||||||
|
const markLineLineStyleWidth = this.isMobile ? 0.15 : 0.3;
|
||||||
|
this.chart = echarts.init(chartContainer, '', { // init图表
|
||||||
|
renderer: 'canvas',
|
||||||
|
useDirtyRect: false,
|
||||||
|
width: 'auto',
|
||||||
|
height: autoheight,
|
||||||
|
});
|
||||||
|
// 获取图表数据
|
||||||
|
let legendData = [];
|
||||||
|
let seriesData = [];
|
||||||
|
chartData.forEach((item,key)=> {
|
||||||
|
let loss = 0;
|
||||||
|
let totalLossRate = 0;
|
||||||
|
let legendName = '';
|
||||||
|
let data = { main: [], markLine: []};
|
||||||
|
item.avg_delay.forEach((avgDelay, index) => {
|
||||||
|
const threshold = 0.9 * MaxTCPPingValue; // 定义阀值,用于判断是否丢包
|
||||||
|
const filterAvgDelay = item.avg_delay.filter(value => value !== 0 && value !== MaxTCPPingValue);
|
||||||
|
const max = Math.max(...filterAvgDelay).toFixed(1);
|
||||||
|
const autoAvgDelay = 1.05 * max > 0.91 * MaxTCPPingValue ? 1.05 * max : 0.91 * MaxTCPPingValue;
|
||||||
|
// 定义丢包 1. avgDelay==0 2. avgDelay>=MaxTCPPingValue 3. avgDelay>=threshold
|
||||||
|
if(avgDelay == 0 || avgDelay >= MaxTCPPingValue){ //绝对丢包
|
||||||
loss += 1;
|
loss += 1;
|
||||||
datal.push({
|
const lossrate = 100 * loss / (index + 1);
|
||||||
xAxis: monitorInfo.result[i].created_at[j],
|
data['main'].push(
|
||||||
|
[item.created_at[index], autoAvgDelay, lossrate]
|
||||||
|
);
|
||||||
|
data['markLine'].push({
|
||||||
|
xAxis: item.created_at[index],
|
||||||
label: { show: false },
|
label: { show: false },
|
||||||
emphasis: { disabled: true },
|
emphasis: { disabled: true },
|
||||||
lineStyle: {
|
lineStyle: { type: "solid" }
|
||||||
type: "solid",
|
|
||||||
color: rgbaColorBar
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
} else if (avgDelay >= threshold && avgDelay < MaxTCPPingValue){ // 相对丢包
|
||||||
|
loss += 1;
|
||||||
|
const lossrate = 100 * loss / (index + 1);
|
||||||
|
data['main'].push(
|
||||||
|
[item.created_at[index], avgDelay, lossrate]
|
||||||
|
);
|
||||||
|
data['markLine'].push({
|
||||||
|
xAxis: item.created_at[index],
|
||||||
|
label: { show: false },
|
||||||
|
emphasis: { disabled: true },
|
||||||
|
lineStyle: { type: "solid" }
|
||||||
|
});
|
||||||
|
} else { // 未丢包
|
||||||
|
const lossrate = 100 * loss / (index + 1);
|
||||||
|
data['main'].push(
|
||||||
|
[item.created_at[index], avgDelay, lossrate]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
lossRate = ((loss / monitorInfo.result[i].created_at.length) * 100).toFixed(1);
|
// 处理legendData
|
||||||
if (lossRate > 99) {
|
totalLossRate = ((loss / item.created_at.length) * 100).toFixed(1);
|
||||||
datal = [];
|
legendName = `${item.monitor_name} ${totalLossRate}%`;
|
||||||
}
|
legendData.push(legendName);
|
||||||
legendName = monitorInfo.result[i].monitor_name +" "+ lossRate + "%";
|
// 处理seriesData
|
||||||
tLegendData.push(legendName);
|
seriesData.push(
|
||||||
tSeries.push({
|
{
|
||||||
name: legendName,
|
name: legendName,
|
||||||
type: 'line',
|
type: 'line',
|
||||||
smooth: true,
|
smooth: true,
|
||||||
symbol: 'none',
|
symbol: 'none',
|
||||||
data: data,
|
connectNulls: true,
|
||||||
|
lineStyle: {
|
||||||
|
width: lineStyleWidth
|
||||||
|
},
|
||||||
|
data: data['main'],
|
||||||
markLine: {
|
markLine: {
|
||||||
symbol: "none",
|
symbol: "none",
|
||||||
symbolSize :0,
|
symbolSize :0,
|
||||||
data: datal
|
data: data['markLine'],
|
||||||
|
itemStyle: {
|
||||||
|
opacity: markLineItemStyleOpacity
|
||||||
|
},
|
||||||
|
lineStyle:{
|
||||||
|
width: markLineLineStyleWidth
|
||||||
|
}
|
||||||
},
|
},
|
||||||
markPoint: {
|
markPoint: {
|
||||||
data: [
|
data: [
|
||||||
{ type: 'max', symbol: 'pin', name: 'Max', itemStyle: { color: rgbaColorMarker }, symbolSize: 30, label: { fontSize: 8 } },
|
{
|
||||||
{ type: 'min', symbol: 'pin', name: 'Min', itemStyle: { color: rgbaColorMarker }, symbolSize: 30, label: { fontSize: 8, offset: [0, 7.5] }, symbolRotate: 180 }
|
name: 'Max',
|
||||||
|
type: 'max',
|
||||||
|
symbol: 'pin',
|
||||||
|
itemStyle: {
|
||||||
|
opacity: markPointItemStyleOpacity
|
||||||
|
},
|
||||||
|
symbolSize: 30,
|
||||||
|
label: {
|
||||||
|
fontSize: 8,
|
||||||
|
color: markPointLabelColor,
|
||||||
|
formatter: function (params) {
|
||||||
|
return Math.round(params.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Min',
|
||||||
|
type: 'min',
|
||||||
|
symbol: 'pin',
|
||||||
|
itemStyle: {
|
||||||
|
opacity: markPointItemStyleOpacity
|
||||||
|
},
|
||||||
|
symbolSize: 30,
|
||||||
|
label: {
|
||||||
|
fontSize: 8,
|
||||||
|
color: markPointLabelColor,
|
||||||
|
offset: [0, 8],
|
||||||
|
formatter: function (params) {
|
||||||
|
return Math.round(params.value);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
symbolRotate: 180
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
const maxLegendsPerRowMobile = localStorage.getItem("maxLegendsPerRowMobile") ? localStorage.getItem("maxLegendsPerRowMobile") : 3;
|
||||||
|
const maxLegendsPerRowPc = localStorage.getItem("maxLegendsPerRowPc") ? localStorage.getItem("maxLegendsPerRowPc") : 6;
|
||||||
|
const autoIncrement = Math.floor((legendData.length - 1) / (this.isMobile ? maxLegendsPerRowMobile : maxLegendsPerRowPc)) * (this.isMobile ? 20 : 28)
|
||||||
|
const height = autoheight + autoIncrement;
|
||||||
|
const gridTop = 40 + autoIncrement;
|
||||||
|
const legendIcon = this.isMobile ? 'rect' : "";
|
||||||
|
const itemWidth = this.isMobile ? 10 : 25;
|
||||||
|
const itemHeight = this.isMobile ? 10 : 14;
|
||||||
|
this.chart.resize({
|
||||||
|
width: 'auto',
|
||||||
|
height: height
|
||||||
|
});
|
||||||
|
// 设置图表配置项
|
||||||
|
const option = {
|
||||||
|
// 全局调色盘
|
||||||
|
color: this.colors,
|
||||||
|
// 背景颜色
|
||||||
|
backgroundColor: backgroundColor,
|
||||||
|
// 文字样式
|
||||||
|
textStyle: {
|
||||||
|
fontSize: fontSize,
|
||||||
|
color: fontColor
|
||||||
|
},
|
||||||
|
// 图表网格设置
|
||||||
|
grid: {
|
||||||
|
top: gridTop,
|
||||||
|
left: gridLeft,
|
||||||
|
right: gridRight
|
||||||
|
},
|
||||||
|
// 图表标题设置
|
||||||
|
title: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
// 图表系列数据设置
|
||||||
|
series: seriesData.flat(),
|
||||||
|
// X轴设置
|
||||||
|
xAxis: {
|
||||||
|
type: 'time',
|
||||||
|
axisLabel: {
|
||||||
|
textStyle: {
|
||||||
|
fontSize: fontSize
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// Y轴设置
|
||||||
|
yAxis: {
|
||||||
|
type: 'value',
|
||||||
|
axisLabel: {
|
||||||
|
textStyle: {
|
||||||
|
fontSize: fontSize
|
||||||
|
}
|
||||||
|
},
|
||||||
|
splitLine: {
|
||||||
|
lineStyle: {
|
||||||
|
width: splitLineWidth,
|
||||||
|
color: splitLineColor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// 图例设置
|
||||||
|
legend: {
|
||||||
|
data: legendData,
|
||||||
|
show: true,
|
||||||
|
icon: legendIcon,
|
||||||
|
textStyle: {
|
||||||
|
fontSize: fontSize,
|
||||||
|
color: fontColor
|
||||||
|
},
|
||||||
|
top: legendTop,
|
||||||
|
bottom: 0,
|
||||||
|
left: legendLeft,
|
||||||
|
padding: legendPadding,
|
||||||
|
itemWidth: itemWidth,
|
||||||
|
itemHeight: itemHeight,
|
||||||
|
},
|
||||||
|
// 提示框设置
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'axis',
|
||||||
|
backgroundColor: tooltipBackgroundColor,
|
||||||
|
borderColor: tooltipBorderColor,
|
||||||
|
textStyle: {
|
||||||
|
fontSize: fontSize,
|
||||||
|
color: fontColor
|
||||||
|
},
|
||||||
|
formatter: function (params) {
|
||||||
|
let tooltipContent = '';
|
||||||
|
const formattedTime = new Date(params[0].value[0]).toLocaleString();
|
||||||
|
tooltipContent += `<span style="line-height:2em">${formattedTime}</span><br>`;
|
||||||
|
params.forEach(param => {
|
||||||
|
const formattedTime = new Date(param.value[0]).toLocaleString();
|
||||||
|
if (!param.seriesName.includes('stack')) {
|
||||||
|
const name = param.seriesName.replace(/\s\d+(\.\d+)?%$/, '');
|
||||||
|
tooltipContent += `<span style="line-height:2em">${param.marker} ${name} ${param.value[2].toFixed(1)}% ${param.value[1].toFixed(2)}</span><br>`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return tooltipContent;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// 数据缩放设置
|
||||||
|
dataZoom: [
|
||||||
|
{
|
||||||
|
type: 'slider',
|
||||||
|
start: 0,
|
||||||
|
end: 100
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
// 设置图表的配置选项
|
||||||
|
this.chart.setOption(option);
|
||||||
|
},
|
||||||
|
reloadCharts() { // 重新加载所有图表
|
||||||
|
this.servers.forEach(node => {
|
||||||
|
const id = node.ID;
|
||||||
|
const chartData = this.chartDataList[id];
|
||||||
|
if (chartData) {
|
||||||
|
this.renderCharts(id,true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
disposeCharts(){
|
||||||
|
if(this.chart) {
|
||||||
|
this.chart.dispose();
|
||||||
|
this.chart = null;
|
||||||
}
|
}
|
||||||
this.option.title.text = monitorInfo.result[0].server_name;
|
|
||||||
this.option.series = tSeries;
|
|
||||||
this.option.legend.data = tLegendData;
|
|
||||||
this.myChart.clear();
|
|
||||||
this.myChart.setOption(this.option);
|
|
||||||
},
|
},
|
||||||
isWindowsPlatform(str) {
|
getServerCountryCode(id){
|
||||||
return str.includes('Windows')
|
const result = this.servers.find(item => item.ID == id);
|
||||||
|
return result.Host.CountryCode ? result.Host.CountryCode : 'rb';
|
||||||
},
|
},
|
||||||
renderChart() {
|
initSearch() {
|
||||||
this.myChart = echarts.init(this.$refs.chartDom);
|
$('#dropdown-search').on('keyup', function() {
|
||||||
this.myChart.setOption(this.option);
|
var searchTerm = $(this).val().toLowerCase();
|
||||||
},
|
$('.dropdown-menu .dropdown-item').each(function() {
|
||||||
},
|
var text = $(this).text().toLowerCase();
|
||||||
beforeDestroy() {
|
if (text.indexOf(searchTerm) > -1) {
|
||||||
this.myChart.dispose();
|
$(this).removeClass('hidden').addClass('visible'); // 显示元素
|
||||||
this.myChart = null;
|
} else {
|
||||||
},
|
$(this).removeClass('visible').addClass('hidden'); // 隐藏元素
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
Loading…
Reference in New Issue
Block a user