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", "name": "nazhua",
"version": "0.4.16", "version": "0.4.18",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",

View File

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

View File

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

View File

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

View File

@ -18,6 +18,7 @@ import {
const defaultState = () => ({ const defaultState = () => ({
init: false, init: false,
serverTime: 0,
serverGroup: [], serverGroup: [],
serverList: [], serverList: [],
serverCount: { serverCount: {
@ -50,6 +51,9 @@ let firstSetServers = true;
const store = createStore({ const store = createStore({
state: defaultState(), state: defaultState(),
mutations: { mutations: {
SET_SERVER_TIME(state, time) {
state.serverTime = time;
},
SET_SERVER_GROUP(state, serverGroup) { SET_SERVER_GROUP(state, serverGroup) {
state.serverGroup = serverGroup; state.serverGroup = serverGroup;
}, },
@ -153,6 +157,9 @@ const store = createStore({
}) { }) {
msg.on('servers', (res) => { msg.on('servers', (res) => {
if (res) { if (res) {
if (res.now) {
commit('SET_SERVER_TIME', res.now);
}
const servers = res.servers?.map?.((i) => { const servers = res.servers?.map?.((i) => {
const item = { const item = {
...i, ...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; line-height: 20px;
font-size: 12px; font-size: 12px;
color: var(--public-note-tag-color); 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; border-radius: 4px;
} }
} }

View File

@ -16,6 +16,7 @@
title="是否自动刷新" title="是否自动刷新"
@click="switchRefresh" @click="switchRefresh"
> >
<span class="label-text">刷新</span>
<div <div
class="switch-box" class="switch-box"
:class="{ :class="{
@ -24,13 +25,13 @@
> >
<span class="switch-dot" /> <span class="switch-dot" />
</div> </div>
<span class="label-text">刷新</span>
</div> </div>
<div <div
class="peak-shaving-group" class="peak-shaving-group"
title="过滤太高或太低的数据" title="过滤太高或太低的数据"
@click="switchPeakShaving" @click="switchPeakShaving"
> >
<span class="label-text">削峰</span>
<div <div
class="switch-box" class="switch-box"
:class="{ :class="{
@ -39,7 +40,28 @@
> >
<span class="switch-dot" /> <span class="switch-dot" />
</div> </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> </div>
</div> </div>
@ -97,6 +119,7 @@ import {
onMounted, onMounted,
onUnmounted, onUnmounted,
} from 'vue'; } from 'vue';
import { useStore } from 'vuex';
import config from '@/config'; import config from '@/config';
import request from '@/utils/request'; import request from '@/utils/request';
import validate from '@/utils/validate'; 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 refreshData = ref(true);
const peakShaving = ref(false); const peakShaving = ref(false);
const showCates = ref({}); const showCates = ref({});
const monitorData = 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(() => { const monitorChartData = computed(() => {
/** /**
* 处理监控数据以生成分类的平均延迟随时间变化的列表 * 处理监控数据以生成分类的平均延迟随时间变化的列表
@ -149,17 +206,25 @@ const monitorChartData = computed(() => {
avgs: [], 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 { const {
threshold, threshold,
mean, mean,
max, max,
min, min,
} = peakShaving.value ? getThreshold(i.avg_delay, 2) : {}; } = peakShaving.value ? getThreshold(showAvgDelay, 2) : {};
i.created_at.forEach((o, index) => { showCreateTime.forEach((o, index) => {
if (dateMap[o]) { if (dateMap[o]) {
return; return;
} }
const avgDelay = i.avg_delay[index]; const avgDelay = showAvgDelay[index];
if (peakShaving.value) { if (peakShaving.value) {
if (avgDelay === 0) { if (avgDelay === 0) {
return; return;
@ -190,22 +255,26 @@ const monitorChartData = computed(() => {
if (!validate.hasOwn(showCates.value, id)) { if (!validate.hasOwn(showCates.value, id)) {
showCates.value[id] = true; 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 = { const cateItem = {
id, id,
name: i, name: i,
color, color,
avg: (avgs.reduce((a, b) => a + b[1], 0) / avgs.length).toFixed(2) * 1, avg: avg.toFixed(2) * 1,
over: ((avgs.filter((o) => o[1] > 0).length / avgs.length) * 100).toFixed(2) * 1, over: (over * 100).toFixed(2) * 1,
}; };
if (Number.isNaN(cateItem.avg)) { if (Number.isNaN(cateItem.avg)) {
cateItem.avg = 0; cateItem.avg = 0;
} }
const titles = [ const titles = [
cateItem.name, cateItem.name,
`平均延迟:${cateItem.avg}ms`, cateItem.avg === 0 ? '' : `平均延迟:${cateItem.avg}ms`,
`执行成功:${cateItem.over}%`, `成功${cateItem.over}%`,
]; ];
cateItem.title = titles.join('\n'); cateItem.title = titles.filter((s) => s).join('\n');
cateList.push(cateItem); cateList.push(cateItem);
valueList.push({ valueList.push({
id, id,
@ -238,6 +307,10 @@ function switchRefresh() {
refreshData.value = !refreshData.value; refreshData.value = !refreshData.value;
} }
function toggleMinute(value) {
minute.value = value;
}
function toggleShowCate(id) { function toggleShowCate(id) {
showCates.value[id] = !showCates.value[id]; showCates.value[id] = !showCates.value[id];
} }
@ -299,12 +372,14 @@ onUnmounted(() => {
.module-head-group { .module-head-group {
display: flex; display: flex;
flex-wrap: wrap;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
gap: 10px; gap: 10px;
height: 30px;
.module-title { .module-title {
width: max-content;
height: 30px;
line-height: 30px; line-height: 30px;
font-size: 16px; font-size: 16px;
color: #eee; color: #eee;
@ -312,15 +387,16 @@ onUnmounted(() => {
.right-box { .right-box {
display: flex; display: flex;
flex-wrap: wrap;
align-items: center; align-items: center;
gap: 10px; gap: 12px;
} }
.peak-shaving-group, .peak-shaving-group,
.refresh-data-group { .refresh-data-group {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 6px; gap: 4px;
cursor: pointer; cursor: pointer;
@media screen and (max-width: 1024px) { @media screen and (max-width: 1024px) {
@ -351,6 +427,7 @@ onUnmounted(() => {
.switch-dot { .switch-dot {
left: 16px; left: 16px;
box-shadow: 1px 1px 2px #000;
} }
} }
} }
@ -360,6 +437,83 @@ onUnmounted(() => {
font-size: 12px; 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 { .monitor-cate-group {

View File

@ -109,7 +109,8 @@ const tagList = computed(() => {
if (props?.info?.PublicNote?.planDataMod?.extra) { if (props?.info?.PublicNote?.planDataMod?.extra) {
list.push(...props.info.PublicNote.planDataMod.extra.split(',')); list.push(...props.info.PublicNote.planDataMod.extra.split(','));
} }
return list; // 5
return list.slice(0, 5);
}); });
const show = computed(() => { const show = computed(() => {
@ -186,7 +187,8 @@ const show = computed(() => {
line-height: 20px; line-height: 20px;
font-size: 12px; font-size: 12px;
color: var(--public-note-tag-color); 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; 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]) { * 将十六进制颜色转换为 RGB 数组
return lineNameColorMap[name]; * @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({ const { color } = uniqolor.random({
saturation: [75, 90], saturation: [75, 90],
lightness: [65, 70], lightness: [65, 70],
differencePoint: 100, differencePoint: 100,
}); });
lineNameColorMap[name] = color; if (lineColors.includes(color)) {
lineColorNameMap[color] = name; 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; return color;
} }

View File

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