Compare commits

...

7 Commits

Author SHA1 Message Date
hi2hi
8a53dcbb0f 🚀 0.4.18 2024-12-12 17:55:29 +00:00
hi2hi
95d1d72cc7 🎨 调整标签颜色;控制列表最多显示5个标签; 2024-12-12 17:55:06 +00:00
hi2hi
a321ce2f69 读取节点名称,设置页面标题 2024-12-12 17:54:23 +00:00
hi2hi
5a771a4932 🐛 平均值计算剔除0 2024-12-12 17:42:06 +00:00
hi2hi
d580f5fc81 🚀 0.4.17 2024-12-12 16:17:08 +00:00
hi2hi
7e416a6f16 网络监控添加最近时间筛选功能 2024-12-12 16:10:53 +00:00
hi2hi
d134e7f2a3 🪄 剔除折线图上的相近颜色 2024-12-12 15:37:17 +00:00
11 changed files with 258 additions and 29 deletions

View File

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

View File

@ -25,8 +25,9 @@
--list-item-price-color: #eee;
--list-item-buy-link-color: #ffc300;
--public-note-tag-color: #ddd;
--public-note-tag-bg: #6a7efc;
--public-note-tag-color: #ccc;
// --public-note-tag-bg: #6a7efc;
--public-note-tag-bg: linear-gradient(125deg, #8f94fb, #4e54c8);
// 针对1440px以下的屏幕
@media screen and (max-width: 1440px) {

View File

@ -78,7 +78,7 @@ export default (
id: 'dataZoomX',
type: 'slider',
xAxisIndex: [0],
filterMode: 'none',
filterMode: 'filter',
}],
yAxis: {
type: 'value',

View File

@ -4,6 +4,7 @@ import {
createWebHashHistory,
} from 'vue-router';
import config from '@/config';
import pageTitle from '@/utils/page-title';
const constantRoutes = [{
name: 'Home',
@ -35,7 +36,7 @@ const routerOptions = {
const router = createRouter(routerOptions);
router.beforeResolve((to, from, next) => {
document.title = [to?.meta?.title, config.nazhua.title].filter((i) => i).join(' - ');
pageTitle(to?.meta?.title);
next();
});

View File

@ -18,6 +18,7 @@ import {
const defaultState = () => ({
init: false,
serverTime: 0,
serverGroup: [],
serverList: [],
serverCount: {
@ -50,6 +51,9 @@ let firstSetServers = true;
const store = createStore({
state: defaultState(),
mutations: {
SET_SERVER_TIME(state, time) {
state.serverTime = time;
},
SET_SERVER_GROUP(state, serverGroup) {
state.serverGroup = serverGroup;
},
@ -153,6 +157,9 @@ const store = createStore({
}) {
msg.on('servers', (res) => {
if (res) {
if (res.now) {
commit('SET_SERVER_TIME', res.now);
}
const servers = res.servers?.map?.((i) => {
const item = {
...i,

5
src/utils/page-title.js Normal file
View File

@ -0,0 +1,5 @@
import config from '@/config';
export default (...args) => {
document.title = [...args, config.nazhua.title].filter((i) => i).join(' - ');
};

View File

@ -595,7 +595,8 @@ const processCount = computed(() => props.info?.State?.ProcessCount);
line-height: 20px;
font-size: 12px;
color: var(--public-note-tag-color);
background-color: var(--public-note-tag-bg);
background: var(--public-note-tag-bg);
text-shadow: 1px 1px 2px rgba(#000, 0.2);
border-radius: 4px;
}
}

View File

@ -16,6 +16,7 @@
title="是否自动刷新"
@click="switchRefresh"
>
<span class="label-text">刷新</span>
<div
class="switch-box"
:class="{
@ -24,13 +25,13 @@
>
<span class="switch-dot" />
</div>
<span class="label-text">刷新</span>
</div>
<div
class="peak-shaving-group"
title="过滤太高或太低的数据"
@click="switchPeakShaving"
>
<span class="label-text">削峰</span>
<div
class="switch-box"
:class="{
@ -39,7 +40,28 @@
>
<span class="switch-dot" />
</div>
<span class="label-text">削峰</span>
</div>
<div class="last-update-time-group">
<span class="last-update-time-label">
最近
</span>
<div class="minutes">
<div
v-for="minuteItem in minutes"
:key="minuteItem.value"
class="minute-item"
:class="{
active: minuteItem.value === minute,
}"
@click="toggleMinute(minuteItem.value)"
>
<span>{{ minuteItem.label }}</span>
</div>
<div
class="active-arrow"
:style="minuteActiveArrowStyle"
/>
</div>
</div>
</div>
</div>
@ -97,6 +119,7 @@ import {
onMounted,
onUnmounted,
} from 'vue';
import { useStore } from 'vuex';
import config from '@/config';
import request from '@/utils/request';
import validate from '@/utils/validate';
@ -115,12 +138,46 @@ const props = defineProps({
},
});
const store = useStore();
const minute = ref(1440);
const minutes = [{
label: '30分钟',
value: 30,
}, {
label: '1小时',
value: 60,
}, {
label: '3小时',
value: 180,
}, {
label: '6小时',
value: 360,
}, {
label: '12小时',
value: 720,
}, {
label: '24小时',
value: 1440,
}];
const refreshData = ref(true);
const peakShaving = ref(false);
const showCates = ref({});
const monitorData = ref([]);
const accpetShowTime = computed(() => {
const now = store.state.serverTime || Date.now();
return now - (minute.value * 60 * 1000);
});
const minuteActiveArrowStyle = computed(() => {
const index = minutes.findIndex((i) => i.value === minute.value);
return {
left: `calc(${index} * var(--minute-item-width))`,
};
});
const monitorChartData = computed(() => {
/**
* 处理监控数据以生成分类的平均延迟随时间变化的列表
@ -149,17 +206,25 @@ const monitorChartData = computed(() => {
avgs: [],
};
}
const showAvgDelay = [];
const showCreateTime = i.created_at.filter((o, index) => {
const status = o >= accpetShowTime.value;
if (status) {
showAvgDelay.push(i.avg_delay[index]);
}
return status;
});
const {
threshold,
mean,
max,
min,
} = peakShaving.value ? getThreshold(i.avg_delay, 2) : {};
i.created_at.forEach((o, index) => {
} = peakShaving.value ? getThreshold(showAvgDelay, 2) : {};
showCreateTime.forEach((o, index) => {
if (dateMap[o]) {
return;
}
const avgDelay = i.avg_delay[index];
const avgDelay = showAvgDelay[index];
if (peakShaving.value) {
if (avgDelay === 0) {
return;
@ -190,22 +255,26 @@ const monitorChartData = computed(() => {
if (!validate.hasOwn(showCates.value, id)) {
showCates.value[id] = true;
}
//
const validAvgs = avgs.filter((a) => a[1] !== 0);
const avg = validAvgs.reduce((a, b) => a + b[1], 0) / validAvgs.length;
const over = avgs.filter((a) => a[1] !== 0).length / avgs.length;
const cateItem = {
id,
name: i,
color,
avg: (avgs.reduce((a, b) => a + b[1], 0) / avgs.length).toFixed(2) * 1,
over: ((avgs.filter((o) => o[1] > 0).length / avgs.length) * 100).toFixed(2) * 1,
avg: avg.toFixed(2) * 1,
over: (over * 100).toFixed(2) * 1,
};
if (Number.isNaN(cateItem.avg)) {
cateItem.avg = 0;
}
const titles = [
cateItem.name,
`平均延迟:${cateItem.avg}ms`,
`执行成功:${cateItem.over}%`,
cateItem.avg === 0 ? '' : `平均延迟:${cateItem.avg}ms`,
`成功${cateItem.over}%`,
];
cateItem.title = titles.join('\n');
cateItem.title = titles.filter((s) => s).join('\n');
cateList.push(cateItem);
valueList.push({
id,
@ -238,6 +307,10 @@ function switchRefresh() {
refreshData.value = !refreshData.value;
}
function toggleMinute(value) {
minute.value = value;
}
function toggleShowCate(id) {
showCates.value[id] = !showCates.value[id];
}
@ -299,12 +372,14 @@ onUnmounted(() => {
.module-head-group {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: 10px;
height: 30px;
.module-title {
width: max-content;
height: 30px;
line-height: 30px;
font-size: 16px;
color: #eee;
@ -312,15 +387,16 @@ onUnmounted(() => {
.right-box {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 10px;
gap: 12px;
}
.peak-shaving-group,
.refresh-data-group {
display: flex;
align-items: center;
gap: 6px;
gap: 4px;
cursor: pointer;
@media screen and (max-width: 1024px) {
@ -351,6 +427,7 @@ onUnmounted(() => {
.switch-dot {
left: 16px;
box-shadow: 1px 1px 2px #000;
}
}
}
@ -360,6 +437,83 @@ onUnmounted(() => {
font-size: 12px;
}
}
.last-update-time-group {
--minute-item-width: 50px;
--minute-item-height: 20px;
display: flex;
align-items: center;
gap: 4px;
.last-update-time-label {
color: #ddd;
height: var(--minute-item-height);
line-height: var(--minute-item-height);
font-size: 12px;
}
@media screen and (max-width: 660px) {
--minute-item-width: 46px;
}
@media screen and (max-width: 600px) {
--minute-item-width: 46px;
}
@media screen and (max-width: 400px) {
.last-update-time-label {
display: none;
}
}
@media screen and (max-width: 330px) {
margin-left: -12px;
}
@media screen and (max-width: 320px) {
margin-left: -18px;
}
}
.minutes {
position: relative;
display: flex;
align-items: center;
// padding: 0 10px;
height: var(--minute-item-height);
background: rgba(#fff, 0.2);
border-radius: calc(var(--minute-item-height) / 2);
.minute-item {
position: relative;
z-index: 10;
width: var(--minute-item-width);
height: var(--minute-item-height);
line-height: var(--minute-item-height);
font-size: 12px;
text-align: center;
cursor: pointer;
color: #aaa;
transition: color 0.3s;
&.active {
color: #fff;
text-shadow: 1px 1px 2px #000;
}
}
.active-arrow {
position: absolute;
top: 0;
left: 0;
width: var(--minute-item-width);
height: var(--minute-item-height);
border-radius: calc(var(--minute-item-height) / 2);
background: #4caf50;
// opacity: 0.5;
transition: left 0.3s;
z-index: 1;
}
}
}
.monitor-cate-group {

View File

@ -109,7 +109,8 @@ const tagList = computed(() => {
if (props?.info?.PublicNote?.planDataMod?.extra) {
list.push(...props.info.PublicNote.planDataMod.extra.split(','));
}
return list;
// 5
return list.slice(0, 5);
});
const show = computed(() => {
@ -186,7 +187,8 @@ const show = computed(() => {
line-height: 20px;
font-size: 12px;
color: var(--public-note-tag-color);
background-color: var(--public-note-tag-bg);
background: var(--public-note-tag-bg);
text-shadow: 1px 1px 2px rgba(#000, 0.2);
border-radius: 4px;
}
}

View File

@ -33,19 +33,74 @@ export function getThreshold(data, tolerance = 2) {
};
}
const lineNameColorMap = {};
const lineColorNameMap = {};
/**
* - 处理相对固定折线的颜色
*/
const lineColorMap = {};
const lineColors = [];
export function getLineColor(name) {
if (lineNameColorMap[name]) {
return lineNameColorMap[name];
}
/**
* 将十六进制颜色转换为 RGB 数组
* @param {string} hex - 十六进制颜色字符串
* @returns {number[]} 返回包含 RGB 数组的对象
*/
function hexToRgb(hex) {
// 去掉可能的前缀 "#"
hex = hex.replace(/^#/, '');
// 将字符串拆分为 r, g, b 三个部分
const bigint = parseInt(hex, 16);
const r = Math.floor(bigint / (256 * 256)) % 256;
const g = Math.floor(bigint / 256) % 256;
const b = bigint % 256;
return [r, g, b];
}
/**
* 计算两个 RGB 颜色之间的距离
* @param {number[]} color1 - 第一个颜色的 RGB 数组
* @param {number[]} color2 - 第二个颜色的 RGB 数组
* @returns {number} 返回两个颜色之间的距离
*/
function rgbDistance(color1, color2) {
const [r1, g1, b1] = color1;
const [r2, g2, b2] = color2;
return Math.abs(r1 - r2) + Math.abs(g1 - g2) + Math.abs(b1 - b2);
}
/**
* 获取一个随机颜色
* @returns {string} 返回一个随机颜色的字符串
*/
function getColor() {
const { color } = uniqolor.random({
saturation: [75, 90],
lightness: [65, 70],
differencePoint: 100,
});
lineNameColorMap[name] = color;
lineColorNameMap[color] = name;
if (lineColors.includes(color)) {
return getColor();
}
if (lineColors.some((i) => rgbDistance(
hexToRgb(i),
hexToRgb(color),
) < 80)) {
return getColor();
}
return color;
}
/**
* 获取线的颜色
* @param {string} name - 线的名称
* @returns {string} 返回线的颜色
*/
export function getLineColor(name) {
// 如果已经有了对应的颜色,直接返回
if (lineColorMap[name]) {
return lineColorMap[name];
}
const color = getColor();
lineColorMap[name] = color;
lineColors.push(color);
return color;
}

View File

@ -54,6 +54,7 @@ import {
alias2code,
locationCode2Info,
} from '@/utils/world-map';
import pageTitle from '@/utils/page-title';
import WorldMap from '@/components/world-map/world-map.vue';
import ServerName from './components/server-detail/server-name.vue';
@ -131,6 +132,7 @@ function handleWorldMapWidth() {
watch(() => info.value, () => {
if (info.value) {
pageTitle(info.value?.Name, '节点详情');
handleWorldMapWidth();
}
});
@ -145,6 +147,7 @@ watch(() => dataInit.value, () => {
onMounted(() => {
if (info.value) {
pageTitle(info.value?.Name, '节点详情');
handleWorldMapWidth();
}
window.addEventListener('resize', handleWorldMapWidth);