Compare commits

...

5 Commits

Author SHA1 Message Date
hi2hi
2d331823b9 🚀 0.7.0 2025-08-12 13:55:53 +08:00
hi2hi
0a33582541 监控图表添加断线显示 2025-08-12 13:17:01 +08:00
hi2hi
9aaa5b0cc3 🪄 调整列表模式的数据列 2025-07-25 17:17:56 +08:00
hi2hi
eed7be4b1b 列表页添加连接数的显示 2025-07-25 17:08:07 +08:00
hi2hi
1b20505ef2 🐳 更新Dockerfile,使用nginx:1.27-alpine替代nginx:1.27.3 2025-06-25 16:04:29 +08:00
13 changed files with 253 additions and 65 deletions

View File

@ -1,4 +1,4 @@
FROM nginx:1.27.3 FROM nginx:1.27-alpine
COPY ./dist /home/wwwroot/html COPY ./dist /home/wwwroot/html
COPY ./nginx-default.conf.template /etc/nginx/templates/default.conf.template COPY ./nginx-default.conf.template /etc/nginx/templates/default.conf.template

View File

@ -1,6 +1,6 @@
{ {
"name": "nazhua", "name": "nazhua",
"version": "0.6.6", "version": "0.7.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",

View File

@ -42,10 +42,9 @@ provide('currentTime', currentTime);
*/ */
function refreshTime() { function refreshTime() {
currentTime.value = Date.now(); currentTime.value = Date.now();
setTimeout(() => { window.requestAnimationFrame(refreshTime);
refreshTime();
}, 1000);
} }
refreshTime();
// Windows // Windows
const isWindows = /windows|win32/i.test(navigator.userAgent); const isWindows = /windows|win32/i.test(navigator.userAgent);

View File

@ -16,12 +16,18 @@
--world-map-point-color: #fff143; --world-map-point-color: #fff143;
--duration-color: #cbf1f5; --duration-color: #89c3eb;
--transfer-color: #f9ed69; --transfer-color: #f9ed69;
--transfer-in-color: var(--transfer-color); --transfer-in-color: var(--transfer-color);
--transfer-out-color: #90f2ff; --transfer-out-color: #90f2ff;
--net-speed-color: #90f2ff;
--net-speed-in-color: #f5b199; --net-speed-in-color: #f5b199;
--net-speed-out-color: #89c3eb; --net-speed-out-color: #89c3eb;
--conn-color: #90f2ff;
--conn-tcp-color: #89c3eb;
--conn-udp-color: #2ca9e1;
--load-color: #90f2ff;
--process-color: #f5b199;
--list-item-price-color: #eee; --list-item-price-color: #eee;
--list-item-buy-link-color: #ffc300; --list-item-buy-link-color: #ffc300;

View File

@ -18,11 +18,13 @@ use([
DataZoomComponent, DataZoomComponent,
]); ]);
export default ( export default (options) => {
const {
dateList, dateList,
valueList, valueList,
mode = 'dark', mode = 'dark',
) => { connectNulls = true,
} = options || {};
const fontFamily = config.nazhua.disableSarasaTermSC === true ? undefined : 'Sarasa Term SC'; const fontFamily = config.nazhua.disableSarasaTermSC === true ? undefined : 'Sarasa Term SC';
const option = { const option = {
darkMode: mode === 'dark', darkMode: mode === 'dark',
@ -36,7 +38,7 @@ export default (
let res = `<p style="font-weight: bold; color: #ff6;">${time}</p>`; let res = `<p style="font-weight: bold; color: #ff6;">${time}</p>`;
if (params.length < 10) { if (params.length < 10) {
params.forEach((i) => { params.forEach((i) => {
res += `${i.marker} ${i.seriesName}: ${i.value[1]}ms<br>`; res += i.value[1] ? `${i.marker} ${i.seriesName}: ${i.value[1]}ms<br>` : '';
}); });
} else { } else {
res += '<table>'; res += '<table>';
@ -45,7 +47,9 @@ export default (
if (index % 2 === 0) { if (index % 2 === 0) {
res += '<tr>'; res += '<tr>';
} }
res += `<td style="padding: 0 4px;">${i.marker} ${i.seriesName}: ${i.value[1]}ms</td>`; res += i.value[1]
? `<td style="padding: 0 4px;">${i.marker} ${i.seriesName}: ${i.value[1]}ms</td>`
: '<td style="padding: 0 4px;"></td>';
if (index % 2 === 1) { if (index % 2 === 1) {
res += '</tr>'; res += '</tr>';
trEnd = true; trEnd = true;
@ -108,7 +112,7 @@ export default (
...i, ...i,
type: 'line', type: 'line',
smooth: true, smooth: true,
connectNulls: true, connectNulls,
legendHoverLink: false, legendHoverLink: false,
symbol: 'none', symbol: 'none',
})), })),

View File

@ -38,15 +38,20 @@ const props = defineProps({
type: [Number, String], type: [Number, String],
default: null, default: null,
}, },
connectNulls: {
type: [Boolean, String],
default: true,
},
}); });
const chartRef = ref(); const chartRef = ref();
const option = computed(() => { const option = computed(() => {
if (props.dateList && props.valueList) { if (props.dateList && props.valueList) {
return lineChart( return lineChart({
props.dateList, dateList: props.dateList,
props.valueList, valueList: props.valueList,
); connectNulls: props.connectNulls,
});
} }
return null; return null;
}); });

View File

@ -118,10 +118,10 @@
{{ cateItem.avg }}ms {{ cateItem.avg }}ms
</span> </span>
<span <span
v-else v-if="cateItem.over !== 0"
class="cate-avg-ms" class="cate-over-rate"
> >
-ms {{ cateItem.over }}%
</span> </span>
</div> </div>
</template> </template>
@ -131,6 +131,7 @@
:date-list="monitorChartData.dateList" :date-list="monitorChartData.dateList"
:value-list="[monitorChartData.valueList[index]]" :value-list="[monitorChartData.valueList[index]]"
:size="240" :size="240"
:connect-nulls="false"
/> />
</div> </div>
</div> </div>
@ -168,12 +169,6 @@
> >
{{ cateItem.avg }}ms {{ cateItem.avg }}ms
</span> </span>
<span
v-else
class="cate-avg-ms"
>
-ms
</span>
</div> </div>
</template> </template>
</popover> </popover>
@ -183,6 +178,7 @@
<line-chart <line-chart
:date-list="monitorChartData.dateList" :date-list="monitorChartData.dateList"
:value-list="monitorChartData.valueList" :value-list="monitorChartData.valueList"
:connect-nulls="false"
/> />
</template> </template>
</dot-dot-box> </dot-dot-box>
@ -263,8 +259,9 @@ const monitorChartType = computed(() => {
return config.nazhua.monitorChartType; return config.nazhua.monitorChartType;
}); });
const now = ref(Date.now()); //
const accpetShowTime = computed(() => now.value - (minute.value * 60 * 1000)); const nowServerTime = computed(() => store.state.serverTime || Date.now());
const accpetShowTime = computed(() => (Math.floor(nowServerTime.value / 60000) - minute.value) * 60000);
const minuteActiveArrowStyle = computed(() => { const minuteActiveArrowStyle = computed(() => {
const index = minutes.findIndex((i) => i.value === minute.value); const index = minutes.findIndex((i) => i.value === minute.value);
@ -302,13 +299,25 @@ const monitorChartData = computed(() => {
}; };
} }
const showAvgDelay = []; const showAvgDelay = [];
const showCreateTime = i.created_at.filter((o, index) => { const showCreateTime = [];
const accpeTimeMap = {};
i.created_at.forEach((o, index) => {
const status = o >= accpetShowTime.value; const status = o >= accpetShowTime.value;
if (status) { if (status) {
showAvgDelay.push(i.avg_delay[index]); accpeTimeMap[o] = i.avg_delay[index];
} }
return status;
}); });
const allMintues = Math.floor((Date.now() - accpetShowTime.value) / 60000);
for (let j = 0; j < allMintues; j += 1) {
const time = accpetShowTime.value + j * 60000;
showCreateTime.push(time);
const timeProp = accpeTimeMap[time];
if (timeProp) {
showAvgDelay.push(timeProp);
} else {
showAvgDelay.push(null);
}
}
const { const {
threshold, threshold,
mean, mean,
@ -316,19 +325,21 @@ const monitorChartData = computed(() => {
min, min,
} = peakShaving.value ? getThreshold(showAvgDelay, 2) : {}; } = peakShaving.value ? getThreshold(showAvgDelay, 2) : {};
showCreateTime.forEach((o, index) => { showCreateTime.forEach((o, index) => {
if (dateMap[o]) { if (Object.prototype.hasOwnProperty.call(dateMap, o)) {
return; return;
} }
const avgDelay = showAvgDelay[index]; const avgDelay = showAvgDelay[index];
if (peakShaving.value) { if (peakShaving.value) {
if (avgDelay === 0) { if (avgDelay === 0) {
dateMap[o] = null;
return; return;
} }
if (Math.abs(avgDelay - mean) > threshold && max / min > 2) { if (Math.abs(avgDelay - mean) > threshold && max / min > 2) {
dateMap[o] = null;
return; return;
} }
} }
dateMap[o] = (avgDelay).toFixed(2) * 1; dateMap[o] = avgDelay ? (avgDelay).toFixed(2) * 1 : null;
}); });
}); });
let dateList = []; let dateList = [];
@ -351,9 +362,9 @@ const monitorChartData = computed(() => {
showCates.value[id] = true; showCates.value[id] = true;
} }
// //
const validAvgs = avgs.filter((a) => a[1] !== 0); const validAvgs = avgs.filter((a) => a[1] !== 0 && a[1] !== null);
const avg = validAvgs.reduce((a, b) => a + b[1], 0) / validAvgs.length; const avg = validAvgs.reduce((a, b) => a + b[1], 0) / validAvgs.length;
const over = avgs.filter((a) => a[1] !== 0).length / avgs.length; const over = avgs.filter((a) => a[1] !== 0 && a[1] !== null).length / avgs.length;
const cateItem = { const cateItem = {
id, id,
name: i, name: i,
@ -384,8 +395,7 @@ const monitorChartData = computed(() => {
}); });
} }
}); });
// dateList = dateList.sort((a, b) => a - b);
dateList = Array.from(new Set(dateList)).sort((a, b) => a - b);
valueList = valueList.filter((i) => showCates.value[i.id]); valueList = valueList.filter((i) => showCates.value[i.id]);
return { return {
dateList, dateList,
@ -410,7 +420,6 @@ function switchChartType() {
} }
function toggleMinute(value) { function toggleMinute(value) {
now.value = store.state.serverTime || Date.now();
minute.value = value; minute.value = value;
} }
@ -454,7 +463,6 @@ async function loadMonitor() {
}).catch((err) => { }).catch((err) => {
console.error(err); console.error(err);
}); });
now.value = store.state.serverTime || Date.now();
} }
let loadMonitorTimer = null; let loadMonitorTimer = null;
@ -539,8 +547,15 @@ onUnmounted(() => {
color: #fff; color: #fff;
} }
.cate-over-rate {
height: var(--cate-item-height);
line-height: calc(var(--cate-item-height) + 2px);
text-align: right;
color: #fffbd8;
}
&.disabled { &.disabled {
filter: grayscale(1); filter: grayscale(1) brightness(0.8);
opacity: 0.5; opacity: 0.5;
} }
} }

View File

@ -91,7 +91,7 @@ const platformLogoIconClassName = computed(() => hostUtils.getPlatformLogoIconCl
const serverRealTimeListTpls = computed(() => { const serverRealTimeListTpls = computed(() => {
if (config.nazhua?.listServerRealTimeShowLoad) { if (config.nazhua?.listServerRealTimeShowLoad) {
return 'duration,load,transfer,speeds'; return 'D-A-T,T-A-U,L-A-P,I-A-O';
} }
return 'duration,transfer,inSpeed,outSpeed'; return 'duration,transfer,inSpeed,outSpeed';
}); });

View File

@ -23,7 +23,10 @@
</template> </template>
<template v-else> <template v-else>
<span class="item-text item-value">{{ value }}</span> <span class="item-text item-value">{{ value }}</span>
<span class="item-text item-unit">{{ unit }}</span> <span
v-if="unit"
class="item-text item-unit"
>{{ unit }}</span>
</template> </template>
</div> </div>
</div> </div>
@ -184,6 +187,12 @@ const columnStyle = computed(() => {
} }
} }
&--speeds {
.item-value {
color: var(--net-speed-color);
}
}
&--inSpeed { &--inSpeed {
.item-value { .item-value {
color: var(--net-speed-in-color); color: var(--net-speed-in-color);
@ -207,5 +216,23 @@ const columnStyle = computed(() => {
color: var(--list-item-price-color); color: var(--list-item-price-color);
} }
} }
&--tcp {
.item-value {
color: var(--conn-tcp-color);
}
}
&--udp {
.item-value {
color: var(--conn-udp-color);
}
}
&--conns {
.item-value {
color: var(--conn-color);
}
}
} }
</style> </style>

View File

@ -41,7 +41,7 @@
<server-list-item-real-time <server-list-item-real-time
v-if="$config.nazhua.hideListItemStat !== true" v-if="$config.nazhua.hideListItemStat !== true"
:info="info" :info="info"
server-real-time-list-tpls="load,inSpeed,outSpeed,transfer,duration" server-real-time-list-tpls="load,conns,speeds,transfer,duration"
/> />
<server-list-item-bill <server-list-item-bill
v-if="$config.nazhua.hideListItemBill !== true" v-if="$config.nazhua.hideListItemBill !== true"

View File

@ -22,13 +22,19 @@
</span> </span>
<span class="item-content-sub-content"> <span class="item-content-sub-content">
<span class="item-value">{{ subItem.show ? subItem?.value : '-' }}</span> <span class="item-value">{{ subItem.show ? subItem?.value : '-' }}</span>
<span class="item-unit item-text">{{ subItem.show ? subItem?.unit : '' }}</span> <span
v-if="subItem.show"
class="item-unit item-text"
>{{ subItem?.unit }}</span>
</span> </span>
</span> </span>
</div> </div>
<template v-else> <template v-else>
<span class="item-value">{{ item.show ? item?.value : '-' }}</span> <span class="item-value">{{ item.show ? item?.value : '-' }}</span>
<span class="item-unit item-text">{{ item.show ? item?.unit : '' }}</span> <span
v-if="item.show"
class="item-unit item-text"
>{{ item?.unit }}</span>
</template> </template>
</div> </div>
<span <span
@ -120,7 +126,14 @@ const {
flex: 1; flex: 1;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 2px; gap: 0.2em;
}
--real-time-label-line-height: calc(var(--real-time-label-font-size, 14px) * 1.8);
.item-content-sub-label {
height: var(--real-time-label-line-height);
line-height: var(--real-time-label-line-height);
} }
.item-content-sub-content { .item-content-sub-content {
@ -128,21 +141,36 @@ const {
align-items: center; align-items: center;
} }
.item-value { .item-value,
line-height: 1em; .item-text,
font-size: var(--real-time-label-font-size, 14px);
}
.item-text {
line-height: 1em;
font-size: var(--real-time-label-font-size, 14px);
}
.item-label { .item-label {
line-height: 1em; height: var(--real-time-label-line-height);
line-height: var(--real-time-label-line-height);
font-size: var(--real-time-label-font-size, 14px); font-size: var(--real-time-label-font-size, 14px);
} }
.item-content-sub-item--L-A-P-load {
.item-value {
color: var(--load-color);
}
}
.item-content-sub-item--L-A-P-process {
.item-value {
color: var(--process-color);
}
}
.item-content-sub-item--D-A-T-duration {
.item-value {
color: var(--duration-color);
}
}
.item-content-sub-item--D-A-T-transfer {
.item-value {
color: var(--transfer-color);
}
}
.item-content-sub-item--speeds-in { .item-content-sub-item--speeds-in {
.item-value { .item-value {
color: var(--net-speed-in-color); color: var(--net-speed-in-color);
@ -153,6 +181,17 @@ const {
color: var(--net-speed-out-color); color: var(--net-speed-out-color);
} }
} }
.item-content-sub-item--conn-tcp {
.item-value {
color: var(--conn-tcp-color);
}
}
.item-content-sub-item--conn-udp {
.item-value {
color: var(--conn-udp-color);
}
}
} }
} }

View File

@ -11,15 +11,15 @@ import uniqolor from 'uniqolor';
*/ */
export function getThreshold(data, tolerance = 2) { export function getThreshold(data, tolerance = 2) {
// 计算数据的平均值 // 计算数据的平均值
const mean = data.reduce((sum, value) => sum + value, 0) / data.length; const mean = data.reduce((sum, value) => sum + (value || 0), 0) / data.length;
// 计算数据的方差 // 计算数据的方差
const variance = data.reduce((sum, value) => sum + (value - mean) ** 2, 0) / data.length; const variance = data.reduce((sum, value) => sum + ((value || 0) - mean) ** 2, 0) / data.length;
// 计算标准差 // 计算标准差
const stdDev = Math.sqrt(variance); const stdDev = Math.sqrt(variance);
// 计算阈值 // 计算阈值
const threshold = tolerance * stdDev; const threshold = tolerance * stdDev;
// 过滤掉值为0的数据 // 过滤掉值为0的数据
const filteredData = data.filter((value) => value !== 0); const filteredData = data.filter((value) => value !== 0 && value !== null);
// 计算过滤后数据的最小值 // 计算过滤后数据的最小值
const min = Math.min(...filteredData); const min = Math.min(...filteredData);
// 计算过滤后数据的最大值 // 计算过滤后数据的最大值

View File

@ -255,6 +255,44 @@ export default (params) => {
show: validate.isSet(netOutSpeed.value?.value), show: validate.isSet(netOutSpeed.value?.value),
}; };
case 'speeds': case 'speeds':
return {
key,
label: '网速',
value: [
`${netInSpeed.value?.value}${netInSpeed.value?.unit}`,
`${netOutSpeed.value?.value}${netOutSpeed.value?.unit}`,
].join('|'),
show: validate.isSet(netInSpeed.value?.value) && validate.isSet(netOutSpeed.value?.value),
};
case 'load':
return {
key,
label: '负载',
value: (props.info.State?.Load1 || 0).toFixed(2),
show: validate.isSet(props.info.State?.Load1),
};
case 'conns':
return {
key,
label: '连接',
value: `${props.info.State?.TcpConnCount || 0}|${props.info.State?.UdpConnCount || 0}`,
show: true,
};
case 'tcp':
return {
key,
label: 'TCP',
value: props.info.State?.TcpConnCount || 0,
show: validate.isSet(props.info.State?.TcpConnCount),
};
case 'udp':
return {
key,
label: 'UDP',
value: props.info.State?.UdpConnCount || 0,
show: validate.isSet(props.info.State?.UdpConnCount),
};
case 'I-A-O':
return { return {
key, key,
label: '网速', label: '网速',
@ -276,13 +314,68 @@ export default (params) => {
], ],
show: validate.isSet(netInSpeed.value?.value) && validate.isSet(netOutSpeed.value?.value), show: validate.isSet(netInSpeed.value?.value) && validate.isSet(netOutSpeed.value?.value),
}; };
case 'load': case 'L-A-P':
return { return {
key, key,
label: '负载', label: '负载',
values: [
{
key: 'load',
label: '负载',
value: (props.info.State?.Load1 || 0).toFixed(2), value: (props.info.State?.Load1 || 0).toFixed(2),
unit: '',
show: validate.isSet(props.info.State?.Load1), show: validate.isSet(props.info.State?.Load1),
},
{
key: 'process',
label: '进程',
value: props.info.State?.ProcessCount || 0,
show: validate.isSet(props.info.State?.ProcessCount),
},
],
show: validate.isSet(props.info.State?.Load1) || validate.isSet(props.info.State?.ProcessCount),
};
case 'T-A-U':
return {
key,
label: '连接',
values: [
{
key: 'tcp',
label: 'TCP',
value: (props.info.State?.TcpConnCount || 0).toString().padEnd(3, ' '),
show: validate.isSet(props.info.State?.TcpConnCount),
},
{
key: 'udp',
label: 'UDP',
value: (props.info.State?.UdpConnCount || 0).toString().padEnd(3, ' '),
show: validate.isSet(props.info.State?.UdpConnCount),
},
],
show: validate.isSet(props.info.State?.TcpConnCount) || validate.isSet(props.info.State?.UdpConnCount),
};
case 'D-A-T':
return {
key,
label: '统计',
values: [
{
key: 'duration',
label: '在线',
value: duration.value?.value,
unit: duration.value?.unit,
show: validate.isSet(duration.value?.value),
},
{
key: 'transfer',
label: '流量',
title: `${transfer.value.statTypeLabel}流量`,
value: transfer.value?.value,
unit: transfer.value?.unit,
show: validate.isSet(transfer.value?.value),
},
],
show: validate.isSet(duration.value?.value) || validate.isSet(transfer.value?.value),
}; };
default: default:
} }