Compare commits

...

5 Commits

Author SHA1 Message Date
hi2hi
066aaaa088 新增行列的单独组件 2024-12-14 06:30:56 +00:00
hi2hi
a2b55c5cca 🚀 0.4.20 2024-12-14 06:30:56 +00:00
hi2hi
94f602cbb5 🪄 优化设置now“当前时间”的时机;多项数据监控列表弹出层优化; 2024-12-14 06:30:56 +00:00
hi2hi
dbe5e5fd02 💥修复getColor死循环 2024-12-14 06:30:56 +00:00
hi2hi
e278637525 🐛 哪吒v1的load1\load5\load15为0不返回,需要配置默认值 2024-12-14 06:30:56 +00:00
10 changed files with 382 additions and 244 deletions

View File

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

View File

@ -36,10 +36,29 @@ export default (
}, },
formatter: (params) => { formatter: (params) => {
const time = dayjs(parseInt(params[0].axisValue, 10)).format('YYYY.MM.DD HH:mm'); const time = dayjs(parseInt(params[0].axisValue, 10)).format('YYYY.MM.DD HH:mm');
let res = `${time}<br>`; let res = `<p style="font-weight: bold; color: #ff6;">${time}</p>`;
params.forEach((i) => { if (params.length < 10) {
res += `${i.marker} ${i.seriesName}: ${i.value[1]}ms<br>`; params.forEach((i) => {
}); res += `${i.marker} ${i.seriesName}: ${i.value[1]}ms<br>`;
});
} else {
res += '<table>';
let trEnd = false;
params.forEach((i, index) => {
if (index % 2 === 0) {
res += '<tr>';
}
res += `<td style="padding: 0 4px;">${i.marker} ${i.seriesName}: ${i.value[1]}ms</td>`;
if (index % 2 === 1) {
res += '</tr>';
trEnd = true;
}
});
if (!trEnd) {
res += '</tr>';
}
res += '</table>';
}
return res; return res;
}, },
backgroundColor: mode === 'dark' ? 'rgba(0, 0, 0, 0.7)' : 'rgba(255, 255, 255, 0.7)', backgroundColor: mode === 'dark' ? 'rgba(0, 0, 0, 0.7)' : 'rgba(255, 255, 255, 0.7)',

View File

@ -2,6 +2,7 @@
* V1版数据加载 * V1版数据加载
*/ */
import store from '@/store'; import store from '@/store';
import validate from '@/utils/validate';
import { Mapping } from '@/utils/object-mapping'; import { Mapping } from '@/utils/object-mapping';
/** /**
@ -97,6 +98,14 @@ export default function (v1Data) {
break; break;
case '_$mapping': case '_$mapping':
v0Data[key] = Mapping.each(magics[$magic[1]], v1Data); v0Data[key] = Mapping.each(magics[$magic[1]], v1Data);
if (key === 'State') {
// 修复Load1、Load5、Load15字段为空时的问题
['Load1', 'Load5', 'Load15'].forEach((k) => {
if (!validate.isSet(v0Data[key][k])) {
v0Data[key][k] = 0;
}
});
}
break; break;
default: default:
break; break;

View File

@ -166,10 +166,8 @@ const showCates = ref({});
const monitorData = ref([]); const monitorData = ref([]);
const accpetShowTime = computed(() => { const now = ref(Date.now());
const now = store.state.serverTime || Date.now(); const accpetShowTime = computed(() => now.value - (minute.value * 60 * 1000));
return now - (minute.value * 60 * 1000);
});
const minuteActiveArrowStyle = computed(() => { const minuteActiveArrowStyle = computed(() => {
const index = minutes.findIndex((i) => i.value === minute.value); const index = minutes.findIndex((i) => i.value === minute.value);
@ -308,6 +306,7 @@ function switchRefresh() {
} }
function toggleMinute(value) { function toggleMinute(value) {
now.value = store.state.serverTime || Date.now();
minute.value = value; minute.value = value;
} }
@ -328,6 +327,7 @@ 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;

View File

@ -0,0 +1,162 @@
<template>
<div
class="list-column"
:class="`list-column--${prop}`"
:style="columnStyle"
>
<div class="list-column-content">
<span class="item-label">{{ label }}</span>
<div class="item-content">
<template v-if="slotContent">
<slot />
</template>
<template v-if="slotValue">
<span class="item-text item-value">
<slot name="value" />
</span>
<span class="item-text item-unit">
<slot name="unit" />
</span>
</template>
<template v-else>
<span class="item-text item-value">{{ value }}</span>
<span class="item-text item-unit">{{ unit }}</span>
</template>
</div>
</div>
</div>
</template>
<script setup>
/**
* 服务器信息列表列
*/
import {
computed,
} from 'vue';
const props = defineProps({
prop: {
type: String,
default: '',
},
label: {
type: String,
default: '',
},
value: {
type: [String, Number],
default: '',
},
unit: {
type: String,
default: '',
},
width: {
type: [String, Number],
default: null,
},
slotContent: {
type: [String, Boolean],
default: false,
},
slotValue: {
type: [String, Boolean],
default: false,
},
});
const columnStyle = computed(() => {
const style = {};
const width = parseInt(props.width, 10);
if (Number.isNaN(width) === false) {
style.width = `${width}px`;
}
return style;
});
</script>
<style lang="scss" scoped>
.list-column {
width: 50px;
.list-column-content {
--list-column-label-height: 16px;
--list-column-value-height: 24px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: var(--list-item-height);
.item-label {
padding-top: 6px; //
line-height: var(--list-column-label-height);
font-size: 12px;
color: #bbb;
}
.item-content {
line-height: var(--list-column-value-height);
font-size: 14px;
}
}
&--duration {
.item-value {
color: var(--duration-color);
}
}
&--load {
.item-value {
color: var(--load-color);
}
}
&--transfer {
width: 80px;
.item-value {
color: var(--transfer-color);
}
}
&--inTransfer {
.item-value {
color: var(--transfer-in-color);
}
}
&--outTransfer {
.item-value {
color: var(--transfer-out-color);
}
}
&--inSpeed {
.item-value {
color: var(--net-speed-in-color);
}
}
&--outSpeed {
.item-value {
color: var(--net-speed-out-color);
}
}
&--remaining-time {
width: 60px;
.value-text {
color: #74dbef;
}
}
&--billing {
width: 60px;
.value-text {
color: var(--list-item-price-color);
}
}
}
</style>

View File

@ -1,49 +1,32 @@
<template> <template>
<div <server-list-column
v-if="extraFields?.remainingTime" v-if="extraFields?.remainingTime"
class="list-column-item list-column-item--remaining-time" prop="remaining-time"
> label="剩余"
<div class="list-column-item-content"> :value="billAndPlan?.remainingTime?.value || '-'"
<span class="item-label">剩余</span> />
<div class="item-content"> <server-list-column
<span class="text-item value-text">{{ billAndPlan?.remainingTime?.value || '-' }}</span>
</div>
</div>
</div>
<div
v-if="extraFields?.billing" v-if="extraFields?.billing"
class="list-column-item list-column-item--billing" prop="billing"
> label="费用"
<div class="list-column-item-content"> :value="billAndPlan?.billing?.value || '-'"
<span />
v-if="!billAndPlan?.billing?.isFree && billAndPlan?.billing?.label" <server-list-column
class="item-label"
>
{{ billAndPlan.billing.label }}
</span>
<div class="item-content">
<span class="text-item value-text">{{ billAndPlan?.billing?.value || '-' }}</span>
</div>
</div>
</div>
<div
v-if="extraFields?.orderLink" v-if="extraFields?.orderLink"
class="list-column-item list-column-item--order-link" prop="order-link"
label="链接"
wdith="80"
:slot-content="true"
> >
<div class="list-column-item-content"> <span
<span class="item-label">链接</span> v-if="showBuyBtn"
<div class="item-content"> class="order-link"
<span @click="toBuy"
v-if="showBuyBtn" >
class="text-item value-text" {{ buyBtnText }}
@click="toBuy" </span>
> <span v-else>-</span>
{{ buyBtnText }} </server-list-column>
</span>
<span v-else>-</span>
</div>
</div>
</div>
</template> </template>
<script setup> <script setup>
@ -59,6 +42,8 @@ import config from '@/config';
import handleServerBillAndPlan from '@/views/composable/server-bill-and-plan'; import handleServerBillAndPlan from '@/views/composable/server-bill-and-plan';
import ServerListColumn from './server-list-column.vue';
const props = defineProps({ const props = defineProps({
info: { info: {
type: Object, type: Object,
@ -81,60 +66,15 @@ const {
const buyBtnText = computed(() => config.nazhua.buyBtnText || '购买'); const buyBtnText = computed(() => config.nazhua.buyBtnText || '购买');
const showBuyBtn = computed(() => !!props.info?.PublicNote?.customData?.orderLink); const showBuyBtn = computed(() => !!props.info?.PublicNote?.customData?.orderLink);
// function toBuy() { function toBuy() {
// const decodeUrl = decodeURIComponent(props.info?.PublicNote?.customData?.orderLink); const decodeUrl = decodeURIComponent(props.info?.PublicNote?.customData?.orderLink);
// window.open(decodeUrl, '_blank'); window.open(decodeUrl, '_blank');
// } }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.list-column-item { .order-link {
color: var(--list-item-buy-link-color);
.list-column-item-content { cursor: pointer;
--item-content-label-height: 16px;
--item-content-value-height: 24px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: var(--list-item-height);
.item-label {
padding-top: 6px; //
line-height: var(--item-content-label-height);
font-size: 12px;
color: #ccc;
}
.item-content {
line-height: var(--item-content-value-height);
font-size: 14px;
}
}
&--remaining-time {
width: 60px;
.value-text {
color: #74dbef;
}
}
&--billing {
width: 60px;
.value-text {
color: var(--list-item-price-color);
}
}
&--order-link {
width: 60px;
.value-text {
color: var(--list-item-buy-link-color);
cursor: pointer;
}
}
} }
</style> </style>

View File

@ -1,18 +1,12 @@
<template> <template>
<div <server-list-column
v-for="item in serverRealTimeList" v-for="item in serverRealTimeList"
:key="item.key" :key="item.key"
class="list-column-item list-column-item--real-time" :prop="item.key"
:class="`list-column-item--real-time-${item.key}`" :label="item.label"
> :value="item.show ? item?.value : '-'"
<div class="real-time-content"> :unit="item.show ? item?.unit : ''"
<span class="item-label">{{ item.label }}</span> />
<div class="item-content">
<span class="item-value">{{ item.show ? item?.value : '-' }}</span>
<span class="item-unit item-text">{{ item.show ? item?.unit : '' }}</span>
</div>
</div>
</div>
</template> </template>
<script setup> <script setup>
@ -24,6 +18,8 @@ import {
} from 'vue'; } from 'vue';
import handleServerRealTime from '@/views/composable/server-real-time'; import handleServerRealTime from '@/views/composable/server-real-time';
import ServerListColumn from './server-list-column.vue';
const props = defineProps({ const props = defineProps({
info: { info: {
type: Object, type: Object,
@ -47,59 +43,3 @@ const {
serverRealTimeListTpls: props.serverRealTimeListTpls, serverRealTimeListTpls: props.serverRealTimeListTpls,
}); });
</script> </script>
<style lang="scss" scoped>
.list-column-item {
.real-time-content {
--real-time-label-height: 16px;
--real-time-value-height: 24px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: var(--list-item-height);
.item-label {
padding-top: 6px; //
line-height: var(--real-time-label-height);
font-size: 12px;
color: #ccc;
}
.item-content {
line-height: var(--real-time-value-height);
font-size: 14px;
}
}
&--real-time-duration {
width: 50px;
.item-value {
color: var(--duration-color);
}
}
&--real-time-load {
width: 50px;
.item-value {
color: var(--load-color);
}
}
&--real-time-transfer {
width: 80px;
.item-value {
color: var(--transfer-color);
}
}
&--real-time-inSpeed {
width: 50px;
.item-value {
color: var(--net-speed-in-color);
}
}
&--real-time-outSpeed {
width: 50px;
.item-value {
color: var(--net-speed-out-color);
}
}
}
</style>

View File

@ -8,43 +8,59 @@
}" }"
@click="openDetail" @click="openDetail"
> >
<div class="list-column-item list-column-item--server-flag"> <div class="row-left-box">
<span <div class="list-column-item list-column-item--server-flag">
class="server-flag"
>
<span <span
class="fi" class="server-flag"
:class="'fi-' + (info?.Host?.CountryCode || 'un')" >
/> <span
</span> class="fi"
:class="'fi-' + (info?.Host?.CountryCode || 'un')"
/>
</span>
</div>
<div class="list-column-item list-column-item--server-name">
<span
class="server-name"
:title="info.Name"
>
{{ info.Name }}
</span>
</div>
<server-list-column
prop="server-flag"
label="地区"
:value="info?.Host?.CountryCode?.toUpperCase() || 'UN'"
/>
<server-list-column
prop="server-system"
label="系统"
:value="platformSystemLabel || '-'"
/>
<server-list-column
prop="cpu-mem"
label="配置"
width="80"
:value="cpuAndMemAndDisk || '-'"
/>
</div> </div>
<div class="list-column-item list-column-item--server-name"> <div class="row-center-box">
<span <server-list-item-status
class="server-name" v-if="$config.nazhua.hideListItemStatusDonut !== true"
:title="info.Name" :info="info"
> />
{{ info.Name }}
</span>
</div> </div>
<div class="list-column-item list-column-item--server-system"> <div class="row-right-box">
<span :class="platformLogoIconClassName" /> <server-list-item-real-time
v-if="$config.nazhua.hideListItemStat !== true"
:info="info"
server-real-time-list-tpls="load,inSpeed,outSpeed,transfer,duration"
/>
<server-list-item-bill
v-if="$config.nazhua.hideListItemBill !== true"
:info="info"
/>
</div> </div>
<div class="list-column-item list-column-item--cpu-mem">
<span class="core-mem">{{ cpuAndMemAndDisk || '-' }}</span>
</div>
<server-list-item-status
v-if="$config.nazhua.hideListItemStatusDonut !== true"
:info="info"
/>
<server-list-item-real-time
v-if="$config.nazhua.hideListItemStat !== true"
:info="info"
server-real-time-list-tpls="load,inSpeed,outSpeed,transfer,duration"
/>
<server-list-item-bill
v-if="$config.nazhua.hideListItemBill !== true"
:info="info"
/>
</dot-dot-box> </dot-dot-box>
</template> </template>
@ -62,6 +78,7 @@ import {
import * as hostUtils from '@/utils/host'; import * as hostUtils from '@/utils/host';
import handleServerInfo from '@/views/composable/server-info'; import handleServerInfo from '@/views/composable/server-info';
import ServerListColumn from './server-list-column.vue';
import ServerListItemStatus from './server-list-item-status.vue'; import ServerListItemStatus from './server-list-item-status.vue';
import ServerListItemRealTime from './server-list-item-real-time.vue'; import ServerListItemRealTime from './server-list-item-real-time.vue';
import ServerListItemBill from './server-list-item-bill.vue'; import ServerListItemBill from './server-list-item-bill.vue';
@ -82,7 +99,7 @@ const { cpuAndMemAndDisk } = handleServerInfo({
props, props,
}); });
const platformLogoIconClassName = computed(() => hostUtils.getPlatformLogoIconClassName(props.info?.Host?.Platform)); const platformSystemLabel = computed(() => hostUtils.getSystemOSLabel(props.info?.Host?.Platform));
function openDetail() { function openDetail() {
router.push({ router.push({
@ -99,7 +116,7 @@ function openDetail() {
--list-item-height: 64px; --list-item-height: 64px;
--list-item-border-radius: 8px; --list-item-border-radius: 8px;
--list-item-gap: 10px; --list-item-gap: 10px;
--list-item-padding: 20px; --list-item-padding: 0;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
@ -110,6 +127,25 @@ function openDetail() {
&--offline { &--offline {
filter: grayscale(1); filter: grayscale(1);
} }
.row-left-box,
.row-center-box,
.row-right-box {
display: flex;
align-items: center;
gap: var(--list-item-gap);
}
.row-left-box,
.row-right-box {
flex: 1;
}
.row-right-box {
justify-content: flex-end;
}
.row-center-box {
justify-content: center;
}
} }
.list-column-item { .list-column-item {
@ -120,6 +156,7 @@ function openDetail() {
&--server-flag { &--server-flag {
--server-flag-size: 24px; --server-flag-size: 24px;
width: calc(var(--server-flag-size) * 1.5); width: calc(var(--server-flag-size) * 1.5);
margin-left: 20px;
.server-flag { .server-flag {
width: calc(var(--server-flag-size) * 1.5); width: calc(var(--server-flag-size) * 1.5);
height: var(--server-flag-size); height: var(--server-flag-size);
@ -128,7 +165,7 @@ function openDetail() {
} }
} }
&--server-name { &--server-name {
flex: 1; width: 200px;
.server-name { .server-name {
height: 32px; height: 32px;
@ -140,27 +177,5 @@ function openDetail() {
overflow: hidden; overflow: hidden;
} }
} }
&--server-system {
width: 24px;
justify-content: center;
font-size: 24px;
}
&--cpu-mem {
width: 100px;
.core-mem {
height: 30px;
line-height: 32px;
font-size: 16px;
width: 100%;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
// 1440px
@media screen and (max-width: 1440px) {
width: 80px;
}
}
} }
</style> </style>

View File

@ -39,9 +39,11 @@ export function getThreshold(data, tolerance = 2) {
const lineColorMap = {}; const lineColorMap = {};
const lineColors = []; const lineColors = [];
const defaultColors = [ const defaultColors = [
'#5470c6', '#91cc75', '#fac858', '#5470C6', '#91CC75', '#FAC858', '#EE6666',
'#ee6666', '#73c0de', '#3ba272', '#73C0DE', '#3BA272', '#FC8452', '#9A60B4',
'#fc8452', '#9a60b4', '#ea7ccc', '#EA7CCC', '#C23531', '#2F4554', '#61A0A8',
'#D48265', '#91C7AE', '#749F83', '#CA8622',
'#BDA29A', '#6E7074', '#546570', '#C4CCD3',
]; ];
/** /**
@ -76,20 +78,24 @@ function rgbDistance(color1, color2) {
* 获取一个随机颜色 * 获取一个随机颜色
* @returns {string} 返回一个随机颜色的字符串 * @returns {string} 返回一个随机颜色的字符串
*/ */
function getColor() { function getColor(count = 0, len = 0) {
// 如果尝试次数超过 3 次,返回固定颜色组里面的颜色
if (count > 3) {
return defaultColors[len % defaultColors.length];
}
const { color } = uniqolor.random({ const { color } = uniqolor.random({
saturation: [75, 90], saturation: [75, 90],
lightness: [65, 70], lightness: [65, 70],
differencePoint: 100, differencePoint: 100,
}); });
if (lineColors.includes(color)) { if (lineColors.includes(color)) {
return getColor(); return getColor(count + 1, len);
} }
if (lineColors.some((i) => rgbDistance( if (lineColors.some((i) => rgbDistance(
hexToRgb(i), hexToRgb(i),
hexToRgb(color), hexToRgb(color),
) < 80)) { ) < 50)) {
return getColor(); return getColor(count + 1, len);
} }
return color; return color;
} }
@ -104,14 +110,7 @@ export function getLineColor(name) {
if (lineColorMap[name]) { if (lineColorMap[name]) {
return lineColorMap[name]; return lineColorMap[name];
} }
// 如果默认颜色还有剩余,直接使用 const color = getColor(0, lineColors.length);
if (defaultColors.length > 0) {
const color = defaultColors.shift();
lineColorMap[name] = color;
lineColors.push(color);
return color;
}
const color = getColor();
lineColorMap[name] = color; lineColorMap[name] = color;
lineColors.push(color); lineColors.push(color);
return color; return color;

View File

@ -122,6 +122,44 @@ export default (params) => {
return result; return result;
}); });
const inTransfer = computed(() => {
const inStats = hostUtils.calcBinary(props.info?.State?.NetInTransfer || 0);
const result = {
value: 0,
unit: '',
};
if (inStats.g > 1) {
result.value = (inStats.g).toFixed(1) * 1;
result.unit = 'G';
} else if (inStats.m > 1) {
result.value = (inStats.m).toFixed(1) * 1;
result.unit = 'M';
} else {
result.value = (inStats.k).toFixed(1) * 1;
result.unit = 'K';
}
return result;
});
const outTransfer = computed(() => {
const outStats = hostUtils.calcBinary(props.info?.State?.NetOutTransfer || 0);
const result = {
value: 0,
unit: '',
};
if (outStats.g > 1) {
result.value = (outStats.g).toFixed(1) * 1;
result.unit = 'G';
} else if (outStats.m > 1) {
result.value = (outStats.m).toFixed(1) * 1;
result.unit = 'M';
} else {
result.value = (outStats.k).toFixed(1) * 1;
result.unit = 'K';
}
return result;
});
/** /**
* 计算入向网速 * 计算入向网速
*/ */
@ -184,6 +222,22 @@ export default (params) => {
unit: transfer.value?.unit, unit: transfer.value?.unit,
show: validate.isSet(transfer.value?.value), show: validate.isSet(transfer.value?.value),
}; };
case 'inTransfer':
return {
key,
label: '入网流量',
value: inTransfer.value?.value,
unit: inTransfer.value?.unit,
show: validate.isSet(inTransfer.value?.value),
};
case 'outTransfer':
return {
key,
label: '出网流量',
value: outTransfer.value?.value,
unit: outTransfer.value?.unit,
show: validate.isSet(outTransfer.value?.value),
};
case 'inSpeed': case 'inSpeed':
return { return {
key, key,
@ -226,7 +280,7 @@ export default (params) => {
return { return {
key, key,
label: '负载', label: '负载',
value: (props.info.State?.Load1 || 0).toFixed(2) * 1, value: (props.info.State?.Load1 || 0).toFixed(2),
unit: '', unit: '',
show: validate.isSet(props.info.State?.Load1), show: validate.isSet(props.info.State?.Load1),
}; };