Compare commits

...

7 Commits

Author SHA1 Message Date
hi2hi
582b367088 🚀 0.4.22 2024-12-18 04:04:52 +00:00
hi2hi
d10386e6e9 row模式在移动端切换为card模式显示 2024-12-18 04:04:23 +00:00
hi2hi
836fddf860 🚀 0.4.21
- 更新readme
 - 调整配置
2024-12-18 02:40:32 +00:00
hi2hi
68bc396ea5 🎨 更新服务器列表样式并为购买链接添加悬停效果
- 为样式中的购买链接添加了新的悬停颜色。
- 调整了服务器列表组件中各列的宽度。
- 从首页视图布局中移除了不必要的内边距。
- 修复了服务器列表项宽度属性中的拼写错误。
- 引入了字段宽度映射,以更好地控制实时项目的布局。
2024-12-18 02:31:50 +00:00
hi2hi
3501483af0 新增行列的单独组件 2024-12-18 02:31:50 +00:00
hi2hi
5ec81d7616 新增"行"模式的服务器列表 2024-12-18 02:31:50 +00:00
hi2hi
26ed6e0722 🐛 设置months的默认值为1,即默认月付 2024-12-18 02:18:14 +00:00
19 changed files with 998 additions and 81 deletions

View File

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

View File

@ -3,6 +3,7 @@ window.$$nazhuaConfig = {
// freeAmount: '白嫖', // 免费服务的费用名称
// infinityCycle: '长期有效', // 无限周期名称
// buyBtnText: '购买', // 购买按钮文案
// listServerItemType: 'row', // 服务器列表项类型 card/row row列表模式移动端自动切换至card
// listServerStatusType: 'progress', // 服务器状态类型--列表
// listServerRealTimeShowLoad: false, // 列表显示服务器实时负载
// detailServerStatusType: 'progress', // 服务器状态类型--详情页

View File

@ -5,7 +5,7 @@
根据不同场景,可以选择是否打包带入或者是否加载这个字体。
## 劝退指南 用前必读
1. 本主题是基于哪吒监控v0版本构建的不确定能否完美v1版本。*20241206的版本已适配*
1. 本主题是基于哪吒监控v0版本构建的~~不确定能否完美v1版本~~。*v0.4.3的版本已适配*
2. 本主题是一个纯前端项目需要解决跨域问题通常需要一个nginx或者caddy反代请求解决跨域问题。
3. 我不会提供任何技术支持如果你有问题可以提issue但是我不保证会回答可能询问GPT会更快。
@ -15,7 +15,9 @@
默认的数据是基于V0
### Release版本的nazhua
V1下载最新版本[Releases](https://github.com/hi2shark/nazhua/releases)的`dist.zip`
V0下载最新版本[Releases](https://github.com/hi2shark/nazhua/releases)的`v0-{版本}-all.zip`或`v0-{版本}-cdn-{CDN供应方}.zip`;
V0下载最新版本[Releases](https://github.com/hi2shark/nazhua/releases)的`v0-dist.zip`;
`v{版本}-all.zip`是包含字体的全量包。
`v{版本}-cdn-{CDN供应方}.zip`是公共资源使用CDN引用的版本。
## 关于点阵地图
点阵地图是一个失真的地图,地图边际与城市位置都不是真实的经纬度坐标,因此无法通过经纬度来定位城市。
@ -181,11 +183,13 @@ server {
window.$$nazhuaConfig = {
title: '哪吒监控', // 网站标题
freeAmount: '白嫖', // 免费服务的费用名称
infinityCycle: '无限', // 无限周期名称
infinityCycle: '长期有效', // 无限周期名称
buyBtnText: '购买', // 购买按钮文案
listServerItemType: 'row', // 服务器列表项类型 card/row row列表模式目前不兼容移动端
listServerStatusType: 'progress', // 服务器状态类型--列表
listServerRealTimeShowLoad: false, // 列表显示服务器实时负载
detailServerStatusType: 'progress', // 服务器状态类型--详情页
serverStatusLinear: true, // 服务器状态渐变线性显示
disableSarasaTermSC: false, // 禁用Sarasa Term SC字体
hideWorldMap: false, // 隐藏地图
hideHomeWorldMap: false, // 隐藏首页地图
@ -197,7 +201,7 @@ window.$$nazhuaConfig = {
hideListItemBill: false, // 隐藏列表项的账单信息
hideFilter: false, // 隐藏筛选
hideTag: false, // 隐藏标签
hideDotBG: false, // 隐藏框框里面的点点背景
hideDotBG: true, // 隐藏框框里面的点点背景
monitorRefreshTime: 10, // 监控刷新时间间隔单位s, 0为不刷新为保证不频繁请求源站最低生效值为10s
filterGPUKeywords: ['Virtual Display'], // 如果GPU名称中包含这些关键字则过滤掉
customCodeMap: {}, // 自定义的地图点信息
@ -212,7 +216,7 @@ window.$$nazhuaConfig = {
v1ApiSettingPath: '/api/v1/setting',
v1ApiProfilePath: '/api/v1/profile',
v1DashboardUrl: '/dashboard', // v1版本控制台地址
v1HideNezhaDashboardBtn: false, // v1版本导航栏控制台入口/登录按钮 在nezhaVersion为v1时有效
v1HideNezhaDashboardBtn: true, // v1版本导航栏控制台入口/登录按钮 在nezhaVersion为v1时有效
routeMode: 'h5', // 路由模式
};
```

View File

@ -25,6 +25,7 @@
--list-item-price-color: #eee;
--list-item-buy-link-color: #ffc300;
--list-item-buy-link-color-hover: #ff9900;
--public-note-tag-color: #ddd;
--public-note-tag-bg: linear-gradient(125deg, #8f94fb, #4e54c8);

View File

@ -21,6 +21,7 @@ const defaultState = () => ({
serverTime: 0,
serverGroup: [],
serverList: [],
serverListColumnWidths: {},
serverCount: {
total: 0,
online: 0,
@ -92,6 +93,9 @@ const store = createStore({
SET_SETTING(state, setting) {
state.setting = setting;
},
SET_SERVER_LIST_COLUMN_WIDTHS(state, widths) {
state.serverListColumnWidths = widths;
},
},
actions: {
/**
@ -184,6 +188,33 @@ const store = createStore({
}
});
},
/**
* 设置服务器列表行宽度
*/
setServerListColumnWidths({
commit,
state,
}, data) {
const newWidths = {
...state.serverListColumnWidths,
...data,
};
commit('SET_SERVER_LIST_COLUMN_WIDTHS', newWidths);
},
setServerListColumnWidth({
commit,
state,
}, data) {
const newWidths = {
...state.serverListColumnWidths,
};
if (newWidths[data.prop]) {
newWidths[data.prop] = Math.max(newWidths[data.prop], data.width);
} else {
newWidths[data.prop] = data.width;
}
commit('SET_SERVER_LIST_COLUMN_WIDTHS', newWidths);
},
},
});

View File

@ -0,0 +1,211 @@
<template>
<div
class="list-column"
:class="`list-column--${prop}`"
:style="columnStyle"
>
<div
ref="columnContentRef"
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,
ref,
onMounted,
onBeforeUnmount,
} from 'vue';
import {
useStore,
} from 'vuex';
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 store = useStore();
const columnContentRef = ref(null);
let resizeObserver = null;
const columnWidth = computed(() => store.state?.serverListColumnWidths?.[props.prop]);
onMounted(() => {
if (columnContentRef.value) {
resizeObserver = new ResizeObserver((entries) => {
entries.forEach((entry) => {
let { width } = entry.contentRect;
width = Math.ceil(width);
store.dispatch('setServerListColumnWidth', {
prop: props.prop,
width: width > 40 ? width : 40,
});
});
});
resizeObserver.observe(columnContentRef.value);
}
});
onBeforeUnmount(() => {
if (resizeObserver) {
resizeObserver.disconnect();
resizeObserver = null;
}
});
const columnStyle = computed(() => {
const style = {};
if (props.width) {
const width = parseInt(props.width, 10);
if (Number.isNaN(width) === false) {
style.width = `${width}px`;
}
} else if (columnWidth.value > 0) {
style.width = `${columnWidth.value}px`;
}
return style;
});
</script>
<style lang="scss" scoped>
.list-column {
--list-column-label-height: 16px;
--list-column-value-height: 24px;
position: relative;
width: auto;
height: calc(var(--list-column-label-height) + var(--list-column-value-height) + 10px);
.list-column-content {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
width: max-content;
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 {
.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 {
.value-text {
color: #74dbef;
}
}
&--billing {
.value-text {
color: var(--list-item-price-color);
}
}
}
</style>

View File

@ -0,0 +1,83 @@
<template>
<server-list-column
v-if="extraFields?.remainingTime"
prop="remaining-time"
label="剩余"
:value="billAndPlan?.remainingTime?.value || '-'"
/>
<server-list-column
v-if="extraFields?.billing"
prop="billing"
label="费用"
:value="billAndPlan?.billing?.value || '-'"
/>
<server-list-column
v-if="extraFields?.orderLink"
prop="order-link"
label="链接"
:slot-content="true"
>
<span
v-if="showBuyBtn"
class="order-link"
@click="toBuy"
>
{{ buyBtnText }}
</span>
<span v-else>-</span>
</server-list-column>
</template>
<script setup>
/**
* 套餐信息
*/
import {
inject,
computed,
} from 'vue';
import config from '@/config';
import handleServerBillAndPlan from '@/views/composable/server-bill-and-plan';
import ServerListColumn from './server-list-column.vue';
const props = defineProps({
info: {
type: Object,
default: () => ({}),
},
});
const filterServerList = inject('filterServerList', {
value: null,
});
const extraFields = computed(() => filterServerList.value?.fields || {});
const {
billAndPlan,
} = handleServerBillAndPlan({
props,
});
const buyBtnText = computed(() => config.nazhua.buyBtnText || '购买');
const showBuyBtn = computed(() => !!props.info?.PublicNote?.customData?.orderLink);
function toBuy() {
const decodeUrl = decodeURIComponent(props.info?.PublicNote?.customData?.orderLink);
window.open(decodeUrl, '_blank');
}
</script>
<style lang="scss" scoped>
.order-link {
color: var(--list-item-buy-link-color);
cursor: pointer;
&:hover {
color: var(--list-item-buy-link-color-hover);
}
}
</style>

View File

@ -0,0 +1,45 @@
<template>
<server-list-column
v-for="item in serverRealTimeList"
:key="item.key"
:prop="item.key"
:label="item.label"
:value="item.show ? item?.value : '-'"
:unit="item.show ? item?.unit : ''"
/>
</template>
<script setup>
/**
* 服务器数据统计
*/
import {
inject,
} from 'vue';
import handleServerRealTime from '@/views/composable/server-real-time';
import ServerListColumn from './server-list-column.vue';
const props = defineProps({
info: {
type: Object,
default: () => ({}),
},
serverRealTimeListTpls: {
type: String,
default: undefined,
},
});
const currentTime = inject('currentTime', {
value: Date.now(),
});
const {
serverRealTimeList,
} = handleServerRealTime({
props,
currentTime,
serverRealTimeListTpls: props.serverRealTimeListTpls,
});
</script>

View File

@ -0,0 +1,143 @@
<template>
<div
class="server-list-item-status-progress"
:class="'server-status--' + type"
:title="valPercent"
>
<span class="progress-label">
{{ label }}
</span>
<div class="progress-bar">
<div class="progress-bar-box">
<div
class="progress-bar-inner"
:style="progressStyle"
/>
<span
class="progress-bar-used"
>
{{ valText }}
</span>
</div>
</div>
</div>
</template>
<script setup>
/**
* 服务器状态进度调单项
*/
import {
computed,
} from 'vue';
const props = defineProps({
type: {
type: String,
default: '',
},
size: {
type: Number,
default: 100,
},
used: {
type: [Number, String],
default: 1,
},
colors: {
type: Object,
default: () => ({}),
},
valText: {
type: String,
default: '',
},
valPercent: {
type: String,
default: '',
},
label: {
type: String,
default: '',
},
content: {
type: [String, Object],
default: '',
},
});
const progressStyle = computed(() => {
const style = {};
style.width = `${Math.min(props.used, 100)}%`;
const color = typeof props.colors === 'string' ? props.colors : props.colors?.used;
if (color) {
if (Array.isArray(color)) {
style.background = `linear-gradient(-35deg, ${color.join(',')})`;
} else {
style.backgroundColor = color;
}
}
return style;
});
</script>
<style lang="scss" scoped>
.server-list-item-status-progress {
--progress-label-height: 16px;
--progress-bar-height: 24px;
--progress-bar-box-height: 14px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: var(--list-item-height);
.progress-label {
padding-top: 6px; //
line-height: var(--progress-label-height);
font-size: 12px;
color: #ccc;
}
.progress-bar {
display: flex;
align-items: center;
width: 100%;
height: var(--progress-bar-height);
}
.progress-bar-box {
position: relative;
width: 100%;
height: var(--progress-bar-box-height);
background: rgba(255, 255, 255, 0.2);
border-radius: calc(var(--progress-bar-box-height) / 2);
overflow: hidden;
}
.progress-bar-inner {
position: absolute;
top: 0;
left: 0;
bottom: 0;
background-color: #08f;
border-radius: calc(var(--progress-bar-box-height) / 2);
box-shadow: 2px 0 2px rgba(#000, 0.2);
}
.progress-bar-used {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
line-height: var(--progress-bar-box-height);
font-size: 12px;
text-align: center;
text-shadow: 1px 1px 2px rgba(#000, 0.8), 0 0 1px rgba(#fff, 0.5);
cursor: default;
}
}
</style>

View File

@ -0,0 +1,71 @@
<template>
<div
v-for="item in serverStatusList"
:key="item.type"
class="list-column-item list-column-item--status"
:class="`list-column-item--status-${componentName} list-column-item--status-type-${item.type}`"
>
<component
:is="componentMaps[componentName]"
:type="item.type"
:used="item.used"
:colors="item.colors"
:val-text="item.valPercent"
:val-percent="`${item.label}使用${item.valText}`"
:label="item.label"
/>
</div>
</template>
<script setup>
/**
* 服务器状态盒子
*/
import config from '@/config';
import handleServerStatus from '@/views/composable/server-status';
import ServerStatusDonut from '@/views/components/server/server-status-donut.vue';
import ServerStatusProgress from './server-list-item-status-progress.vue';
const props = defineProps({
info: {
type: Object,
default: () => ({}),
},
});
const componentMaps = {
donut: ServerStatusDonut,
progress: ServerStatusProgress,
};
const componentName = [
'donut',
'progress',
].includes(config.nazhua.listServerStatusType) ? config.nazhua.listServerStatusType : 'donut';
const {
serverStatusList,
} = handleServerStatus({
props,
statusListTpl: 'cpu,mem,disk',
statusListItemContent: false,
});
</script>
<style lang="scss" scoped>
.list-column-item {
&--status-progress {
width: 72px;
padding: 0 3px;
}
&--status-donut {
--server-status-size: 66px;
--server-status-label-scale: 0.8;
--server-status-val-text-font-size: 16px;
--server-status-label-font-size: 12px;
}
}
</style>

View File

@ -0,0 +1,174 @@
<template>
<dot-dot-box
border-radius="var(--list-item-border-radius)"
padding="var(--list-item-padding)"
class="server-list-row-item"
:class="{
'server-list-row-item--offline': info.online === -1,
}"
@click="openDetail"
>
<div class="list-column-item list-column-item--server-flag">
<span
class="server-flag"
>
<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="配置"
:value="cpuAndMemAndDisk || '-'"
/>
<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>
</template>
<script setup>
/**
* 单节点
*/
import {
computed,
} from 'vue';
import {
useRouter,
} from 'vue-router';
import * as hostUtils from '@/utils/host';
import handleServerInfo from '@/views/composable/server-info';
import ServerListColumn from './server-list-column.vue';
import ServerListItemStatus from './server-list-item-status.vue';
import ServerListItemRealTime from './server-list-item-real-time.vue';
import ServerListItemBill from './server-list-item-bill.vue';
const props = defineProps({
info: {
type: Object,
default: () => ({}),
},
});
const router = useRouter();
/**
* XCore XGB
*/
const { cpuAndMemAndDisk } = handleServerInfo({
props,
});
const platformSystemLabel = computed(() => hostUtils.getSystemOSLabel(props.info?.Host?.Platform));
function openDetail() {
router.push({
name: 'ServerDetail',
params: {
serverId: props.info.ID,
},
});
}
</script>
<style lang="scss" scoped>
.server-list-row-item {
--list-item-height: 64px;
--list-item-border-radius: 8px;
--list-item-gap: 0;
--list-item-padding: 0 20px;
display: flex;
align-items: center;
justify-content: space-between;
height: var(--list-item-height);
gap: var(--list-item-gap);
transition: 0.3s;
&--offline {
filter: grayscale(1);
}
@media (max-width: 1280px) {
--list-item-padding: 0 10px;
}
}
.list-column-item {
display: flex;
align-items: center;
overflow: hidden;
&--server-flag {
--server-flag-size: 24px;
width: calc(var(--server-flag-size) * 1.5);
.server-flag {
width: calc(var(--server-flag-size) * 1.5);
height: var(--server-flag-size);
line-height: var(--server-flag-size);
font-size: var(--server-flag-size);
}
@media (max-width: 1280px) {
display: none;
}
@media (max-width: 1024px) {
display: block;
}
}
&--server-name {
width: 220px;
.server-name {
height: 32px;
line-height: 34px;
font-size: 16px;
width: 100%;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
@media (max-width: 1280px) {
width: 180px;
}
@media (max-width: 1024px) {
width: 300px;
}
}
}
</style>

View File

@ -12,7 +12,7 @@
<template #default>
<div
class="chart-donut-label"
:title="`${(used).toFixed(1) * 1}%`"
:title="valPercent ? valPercent : `${(used).toFixed(1) * 1}%`"
>
<div class="server-status-val-text">
<span>{{ valText }}</span>
@ -73,6 +73,10 @@ defineProps({
type: String,
default: '',
},
valPercent: {
type: String,
default: '',
},
label: {
type: String,
default: '',
@ -103,6 +107,7 @@ defineProps({
flex-direction: column;
align-items: center;
justify-content: center;
transform: scale(var(--server-status-label-scale, 1));
cursor: pointer;
}

View File

@ -25,7 +25,8 @@ export default (params) => {
billingDataMod,
planDataMod,
} = props.info.PublicNote;
let months;
// 默认1个月
let months = 1;
// 套餐资费
let cycleLabel;
if (validate.isSet(billingDataMod?.cycle)) {

View File

@ -122,6 +122,44 @@ export default (params) => {
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,
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':
return {
key,
@ -226,7 +280,7 @@ export default (params) => {
return {
key,
label: '负载',
value: (props.info.State?.Load1 || 0).toFixed(2) * 1,
value: (props.info.State?.Load1 || 0).toFixed(2),
unit: '',
show: validate.isSet(props.info.State?.Load1),
};

View File

@ -65,6 +65,8 @@ export default (params) => {
{
const CoresVal = cpuInfo.value?.cores ? `${cpuInfo.value?.cores}C` : '-';
const usedColor = config.nazhua.serverStatusLinear ? ['#0088FF', '#72B7FF'] : '#0088FF';
const valPercent = `${(props.info.State?.CPU || 0).toFixed(1) * 1}%`;
const valText = valPercent;
return {
type: 'cpu',
used: (props.info.State?.CPU || 0).toFixed(1) * 1,
@ -72,7 +74,8 @@ export default (params) => {
used: usedColor,
total: 'rgba(255, 255, 255, 0.25)',
},
valText: `${(props.info.State?.CPU || 0).toFixed(1) * 1}%`,
valText,
valPercent,
label: 'CPU',
content: {
default: cpuInfo.value?.core || CoresVal,
@ -82,11 +85,11 @@ export default (params) => {
}
case 'mem':
{
let usedVal;
let valText;
if (useMemAndTotalMem.value.used.g >= 10 && useMemAndTotalMem.value.total.g >= 10) {
usedVal = `${(useMemAndTotalMem.value.used.g).toFixed(1) * 1}G`;
valText = `${(useMemAndTotalMem.value.used.g).toFixed(1) * 1}G`;
} else {
usedVal = `${Math.ceil(useMemAndTotalMem.value.used.m)}M`;
valText = `${Math.ceil(useMemAndTotalMem.value.used.m)}M`;
}
let contentVal;
if (useMemAndTotalMem.value.total.g > 4) {
@ -102,7 +105,8 @@ export default (params) => {
used: usedColor,
total: 'rgba(255, 255, 255, 0.25)',
},
valText: usedVal,
valText,
valPercent: `${useMemAndTotalMem.value.usePercent.toFixed(1) * 1}%`,
label: '内存',
content: {
default: `运行内存${contentVal}`,
@ -115,11 +119,11 @@ export default (params) => {
if (!useSwapAndTotalSwap.value) {
return null;
}
let usedVal;
let valText;
if (useSwapAndTotalSwap.value.used.g >= 10 && useSwapAndTotalSwap.value.total.g >= 10) {
usedVal = `${(useSwapAndTotalSwap.value.used.g).toFixed(1) * 1}G`;
valText = `${(useSwapAndTotalSwap.value.used.g).toFixed(1) * 1}G`;
} else {
usedVal = `${Math.ceil(useSwapAndTotalSwap.value.used.m)}M`;
valText = `${Math.ceil(useSwapAndTotalSwap.value.used.m)}M`;
}
let contentVal;
if (useSwapAndTotalSwap.value.total.g > 4) {
@ -135,7 +139,8 @@ export default (params) => {
used: usedColor,
total: 'rgba(255, 255, 255, 0.25)',
},
valText: usedVal,
valText,
valPercent: `${useSwapAndTotalSwap.value.usePercent.toFixed(1) * 1}%`,
label: '交换',
content: {
default: `交换内存${contentVal}`,
@ -160,6 +165,7 @@ export default (params) => {
total: 'rgba(255, 255, 255, 0.25)',
},
valText: `${(useDiskAndTotalDisk.value.used.g).toFixed(1) * 1}G`,
valPercent: `${useDiskAndTotalDisk.value.usePercent.toFixed(1) * 1}%`,
label: '磁盘',
content: {
default: `磁盘容量${contentValue}`,

View File

@ -13,6 +13,10 @@
<div
v-if="showFilter"
class="fitler-group"
:class="{
'list-is-row': showListRow,
'list-is-card': showListCard,
}"
>
<div class="left-box">
<server-option-box
@ -30,12 +34,27 @@
</div>
</div>
<transition-group
v-if="showListRow"
name="list"
tag="div"
class="server-list-container"
:class="`server-list--row`"
>
<server-item
v-for="item in filterServerList"
<server-row-item
v-for="item in filterServerList.list"
:key="item.ID"
:info="item"
/>
</transition-group>
<transition-group
v-if="showListCard"
name="list"
tag="div"
class="server-list-container"
:class="`server-list--card`"
>
<server-card-item
v-for="item in filterServerList.list"
:key="item.ID"
:info="item"
/>
@ -51,6 +70,7 @@
import {
ref,
provide,
computed,
onMounted,
onUnmounted,
@ -66,13 +86,29 @@ import {
count2size,
} from '@/utils/world-map';
import uuid from '@/utils/uuid';
import validate from '@/utils/validate';
import WorldMap from '@/components/world-map/world-map.vue';
import ServerOptionBox from './components/server-list/server-option-box.vue';
import ServerItem from './components/server-list/server-list-item.vue';
import ServerCardItem from './components/server-list/card/server-list-item.vue';
import ServerRowItem from './components/server-list/row/server-list-item.vue';
const store = useStore();
const worldMapWidth = ref();
const windowWidth = ref(window.innerWidth);
const showListRow = computed(() => {
if (windowWidth.value > 1024) {
return config.nazhua.listServerItemType === 'row';
}
return false;
});
const showListCard = computed(() => {
if (windowWidth.value > 1024) {
return config.nazhua.listServerItemType !== 'row';
}
return true;
});
const showFilter = computed(() => config.nazhua.hideFilter !== true);
const filterFormData = ref({
@ -120,45 +156,80 @@ const onlineOptions = computed(() => {
return [];
});
const filterServerList = computed(() => serverList.value.filter((i) => {
const isFilterArr = [];
if (filterFormData.value.tag) {
const group = store.state.serverGroup.find((o) => o.name === filterFormData.value.tag);
isFilterArr.push((group?.servers || []).includes(i.ID));
}
if (filterFormData.value.online) {
isFilterArr.push(i.online === (filterFormData.value.online * 1));
}
return isFilterArr.length ? isFilterArr.every((o) => o) : true;
}));
const filterServerList = computed(() => {
const fields = {};
const locationMap = {};
const list = serverList.value.filter((i) => {
const isFilterArr = [];
if (filterFormData.value.tag) {
const group = store.state.serverGroup.find((o) => o.name === filterFormData.value.tag);
isFilterArr.push((group?.servers || []).includes(i.ID));
}
if (filterFormData.value.online) {
isFilterArr.push(i.online === (filterFormData.value.online * 1));
}
const status = isFilterArr.length ? isFilterArr.every((o) => o) : true;
if (!status) {
return false;
}
//
if (i.PublicNote) {
const {
billingDataMod,
planDataMod,
customData,
} = i.PublicNote;
if (validate.isSet(billingDataMod?.amount)) {
fields.billing = true;
}
if (validate.isSet(billingDataMod?.endDate)) {
fields.remainingTime = true;
}
if (validate.isSet(planDataMod?.bandwidth)) {
fields.bandwidth = true;
}
if (validate.isSet(customData?.orderLink)) {
fields.orderLink = true;
}
}
//
if (i.online === 1) {
let aliasCode;
let locationCode;
if (i?.PublicNote?.customData?.location) {
aliasCode = i.PublicNote.customData.location;
locationCode = i.PublicNote.customData.location;
} else if (i?.Host?.CountryCode) {
aliasCode = i.Host.CountryCode.toUpperCase();
}
const code = alias2code(aliasCode) || locationCode;
if (code) {
if (!locationMap[code]) {
locationMap[code] = [];
}
locationMap[code].push(i);
}
}
return true;
});
return {
fields,
list,
locationMap,
};
});
provide('filterServerList', filterServerList);
/**
* 解构服务器列表的位置数据
*/
const serverLocations = computed(() => {
const locationMap = {};
filterServerList.value.forEach((i) => {
if (i.online === -1) {
return;
}
let aliasCode;
let locationCode;
if (i?.PublicNote?.customData?.location) {
aliasCode = i.PublicNote.customData.location;
locationCode = i.PublicNote.customData.location;
} else if (i?.Host?.CountryCode) {
aliasCode = i.Host.CountryCode.toUpperCase();
}
const code = alias2code(aliasCode) || locationCode;
if (code) {
if (!locationMap[code]) {
locationMap[code] = [];
}
locationMap[code].push(i);
}
});
const locations = [];
Object.entries(locationMap).forEach(([code, servers]) => {
Object.entries(filterServerList.value.locationMap).forEach(([code, servers]) => {
const {
x,
y,
@ -197,6 +268,7 @@ const showWorldMap = computed(() => {
*/
function handleResize() {
worldMapWidth.value = document.querySelector('.server-list-container').clientWidth - 40;
windowWidth.value = window.innerWidth;
}
onMounted(() => {
@ -214,6 +286,35 @@ onUnmounted(() => {
width: 100%;
height: 100%;
.scroll-container {
display: flex;
flex-direction: column;
gap: 10px;
padding: 20px 0;
}
.world-map-box {
position: relative;
display: flex;
align-items: center;
justify-content: center;
}
}
.fitler-group {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
gap: 10px 20px;
width: var(--list-container-width);
margin: auto;
&.list-is-card {
padding: 0 20px;
}
}
.server-list-container.server-list--card {
--list-padding: 20px;
--list-gap-size: 20px;
--list-item-num: 3;
@ -228,29 +329,13 @@ onUnmounted(() => {
)
/ var(--list-item-num)
);
.scroll-container {
display: flex;
flex-direction: column;
gap: 10px;
padding: 20px 0;
}
.world-map-box {
position: relative;
display: flex;
align-items: center;
justify-content: center;
}
.server-list-container {
position: relative;
display: flex;
flex-wrap: wrap;
gap: var(--list-gap-size);
padding: 0 var(--list-padding);
width: var(--list-container-width);
margin: auto;
}
position: relative;
display: flex;
flex-wrap: wrap;
gap: var(--list-gap-size);
padding: 0 var(--list-padding);
width: var(--list-container-width);
margin: auto;
// 1440px
@media screen and (max-width: 1440px) {
@ -266,11 +351,13 @@ onUnmounted(() => {
}
}
.fitler-group {
.server-list-container.server-list--row {
--list-padding: 20px;
--list-gap-size: 12px;
position: relative;
display: flex;
flex-wrap: wrap;
justify-content: space-between;
gap: 10px 20px;
flex-direction: column;
gap: var(--list-gap-size);
width: var(--list-container-width);
margin: auto;
}