mirror of
https://github.com/hi2shark/nazhua.git
synced 2026-01-15 00:30:43 +08:00
Compare commits
No commits in common. "main" and "v0.7.0" have entirely different histories.
5187
package-lock.json
generated
5187
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
21
package.json
21
package.json
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "nazhua",
|
"name": "nazhua",
|
||||||
"version": "0.9.0",
|
"version": "0.7.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
@ -8,16 +8,15 @@
|
|||||||
"build:cdn": "cross-env VITE_SARASA_TERM_SC_USE_CDN=1 VITE_USE_CDN=1 vite build",
|
"build:cdn": "cross-env VITE_SARASA_TERM_SC_USE_CDN=1 VITE_USE_CDN=1 vite build",
|
||||||
"build:nazhua": "cross-env VITE_BASE_PATH=/nazhua/ VITE_NEZHA_VERSION=v0 VITE_SARASA_TERM_SC_USE_CDN=1 VITE_USE_CDN=1 vite build",
|
"build:nazhua": "cross-env VITE_BASE_PATH=/nazhua/ VITE_NEZHA_VERSION=v0 VITE_SARASA_TERM_SC_USE_CDN=1 VITE_USE_CDN=1 vite build",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"lint": "eslint .",
|
"lint": "eslint ."
|
||||||
"lint:fix": "eslint . --fix"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.13.2",
|
"axios": "^1.7.7",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
"echarts": "^5.5.1",
|
"echarts": "^5.5.1",
|
||||||
"flag-icons": "^7.2.3",
|
"flag-icons": "^7.2.3",
|
||||||
"font-logos": "^1.3.0",
|
"font-logos": "^1.3.0",
|
||||||
"remixicon": "^4.7.0",
|
"remixicon": "^4.6.0",
|
||||||
"uniqolor": "^1.1.1",
|
"uniqolor": "^1.1.1",
|
||||||
"vue": "^3.5.12",
|
"vue": "^3.5.12",
|
||||||
"vue-echarts": "^7.0.3",
|
"vue-echarts": "^7.0.3",
|
||||||
@ -25,19 +24,19 @@
|
|||||||
"vuex": "^4.1.0"
|
"vuex": "^4.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.28.5",
|
"@babel/core": "^7.24.9",
|
||||||
"@babel/eslint-parser": "^7.24.8",
|
"@babel/eslint-parser": "^7.24.8",
|
||||||
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.16.7",
|
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.16.7",
|
||||||
"@babel/plugin-proposal-optional-chaining": "^7.21.0",
|
"@babel/plugin-proposal-optional-chaining": "^7.21.0",
|
||||||
"@vitejs/plugin-vue": "^5.2.4",
|
"@vitejs/plugin-vue": "^5.1.4",
|
||||||
"@vue/eslint-config-airbnb": "^7.0.0",
|
"@vue/eslint-config-airbnb": "^7.0.0",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"eslint": "^8.57.1",
|
"eslint": "^8.34.0",
|
||||||
"eslint-plugin-vue": "^9.33.0",
|
"eslint-plugin-vue": "^9.9.0",
|
||||||
"sass": "^1.81.0",
|
"sass": "^1.81.0",
|
||||||
"vite": "^6.4.1",
|
"vite": "^5.4.10",
|
||||||
"vite-plugin-babel": "^1.3.2",
|
"vite-plugin-babel": "^1.2.0",
|
||||||
"vite-plugin-eslint": "^1.8.1",
|
"vite-plugin-eslint": "^1.8.1",
|
||||||
"vite-svg-loader": "^5.1.0"
|
"vite-svg-loader": "^5.1.0"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -11,8 +11,7 @@ window.$$nazhuaConfig = {
|
|||||||
// showLantern: true, // 是否显示灯笼
|
// showLantern: true, // 是否显示灯笼
|
||||||
enableInnerSearch: true, // 启用内部搜索
|
enableInnerSearch: true, // 启用内部搜索
|
||||||
// listServerItemTypeToggle: true, // 服务器列表项类型切换
|
// listServerItemTypeToggle: true, // 服务器列表项类型切换
|
||||||
listServerItemType: 'card', // 服务器列表项类型 card/row/server-status row列表模式移动端自动切换至card
|
// listServerItemType: 'row', // 服务器列表项类型 card/row row列表模式移动端自动切换至card
|
||||||
// serverStatusColumnsTpl: null, // 服务器状态列配置模板
|
|
||||||
// listServerStatusType: 'progress', // 服务器状态类型--列表
|
// listServerStatusType: 'progress', // 服务器状态类型--列表
|
||||||
// listServerRealTimeShowLoad: true, // 列表显示服务器实时负载
|
// listServerRealTimeShowLoad: true, // 列表显示服务器实时负载
|
||||||
// detailServerStatusType: 'progress', // 服务器状态类型--详情页
|
// detailServerStatusType: 'progress', // 服务器状态类型--详情页
|
||||||
@ -31,7 +30,6 @@ window.$$nazhuaConfig = {
|
|||||||
// hideListItemBill: false, // 隐藏列表项的账单信息
|
// hideListItemBill: false, // 隐藏列表项的账单信息
|
||||||
hideListItemLink: true, // 隐藏列表项的购买链接
|
hideListItemLink: true, // 隐藏列表项的购买链接
|
||||||
// hideFilter: false, // 隐藏筛选
|
// hideFilter: false, // 隐藏筛选
|
||||||
// hideSort: false, // 隐藏排序
|
|
||||||
// hideTag: false, // 隐藏标签
|
// hideTag: false, // 隐藏标签
|
||||||
// hideDotBG: true, // 隐藏框框里面的点点背景
|
// hideDotBG: true, // 隐藏框框里面的点点背景
|
||||||
// monitorRefreshTime: 10, // 监控刷新时间间隔,单位s(秒), 0为不刷新,为保证不频繁请求源站,最低生效值为10s
|
// monitorRefreshTime: 10, // 监控刷新时间间隔,单位s(秒), 0为不刷新,为保证不频繁请求源站,最低生效值为10s
|
||||||
|
|||||||
24
src/App.vue
24
src/App.vue
@ -15,7 +15,6 @@ import {
|
|||||||
watch,
|
watch,
|
||||||
provide,
|
provide,
|
||||||
onMounted,
|
onMounted,
|
||||||
onUnmounted,
|
|
||||||
} from 'vue';
|
} from 'vue';
|
||||||
import { useStore } from 'vuex';
|
import { useStore } from 'vuex';
|
||||||
import { useRoute } from 'vue-router';
|
import { useRoute } from 'vue-router';
|
||||||
@ -25,7 +24,6 @@ import config, {
|
|||||||
import sleep from '@/utils/sleep';
|
import sleep from '@/utils/sleep';
|
||||||
import LayoutMain from './layout/main.vue';
|
import LayoutMain from './layout/main.vue';
|
||||||
|
|
||||||
import { WS_CONNECTION_STATUS } from './ws/service';
|
|
||||||
import activeWebsocketService, {
|
import activeWebsocketService, {
|
||||||
wsService,
|
wsService,
|
||||||
restart,
|
restart,
|
||||||
@ -41,16 +39,9 @@ provide('currentTime', currentTime);
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 刷新当前时间
|
* 刷新当前时间
|
||||||
* 使用 requestAnimationFrame 持续更新时间,但只在秒级变化时更新值以减少不必要的响应式更新
|
|
||||||
*/
|
*/
|
||||||
let lastUpdateTime = 0;
|
|
||||||
function refreshTime() {
|
function refreshTime() {
|
||||||
const now = Date.now();
|
currentTime.value = Date.now();
|
||||||
// 只在秒级变化时更新,减少响应式更新频率
|
|
||||||
if (Math.floor(now / 1000) !== Math.floor(lastUpdateTime / 1000)) {
|
|
||||||
currentTime.value = now;
|
|
||||||
lastUpdateTime = now;
|
|
||||||
}
|
|
||||||
window.requestAnimationFrame(refreshTime);
|
window.requestAnimationFrame(refreshTime);
|
||||||
}
|
}
|
||||||
refreshTime();
|
refreshTime();
|
||||||
@ -117,22 +108,17 @@ onMounted(async () => {
|
|||||||
console.log('ws connected');
|
console.log('ws connected');
|
||||||
store.dispatch('watchWsMsg');
|
store.dispatch('watchWsMsg');
|
||||||
});
|
});
|
||||||
const handleFocus = () => {
|
window.addEventListener('focus', () => {
|
||||||
// ws在离开焦点后出现断连,尝试重新连接
|
// ws在离开焦点后出现断连,尝试重新连接
|
||||||
// 仅针对已关闭状态进行重连
|
// 暂定仅针对-1状态进行重连
|
||||||
if (wsService.connected === WS_CONNECTION_STATUS.CLOSED) {
|
if ([-1].includes(wsService.connected)) {
|
||||||
restart();
|
restart();
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
window.addEventListener('focus', handleFocus);
|
|
||||||
/**
|
/**
|
||||||
* 激活websocket服务
|
* 激活websocket服务
|
||||||
*/
|
*/
|
||||||
activeWebsocketService();
|
activeWebsocketService();
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
window.removeEventListener('focus', handleFocus);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
window.addEventListener('unhandledrejection', (event) => {
|
window.addEventListener('unhandledrejection', (event) => {
|
||||||
|
|||||||
@ -28,10 +28,6 @@
|
|||||||
--conn-udp-color: #2ca9e1;
|
--conn-udp-color: #2ca9e1;
|
||||||
--load-color: #90f2ff;
|
--load-color: #90f2ff;
|
||||||
--process-color: #f5b199;
|
--process-color: #f5b199;
|
||||||
--cpu-text-color: #89c3eb;
|
|
||||||
--mem-text-color: #2ca9e1;
|
|
||||||
--disk-text-color: #90f2ff;
|
|
||||||
--swap-text-color: #f5b199;
|
|
||||||
|
|
||||||
--list-item-price-color: #eee;
|
--list-item-price-color: #eee;
|
||||||
--list-item-buy-link-color: #ffc300;
|
--list-item-buy-link-color: #ffc300;
|
||||||
|
|||||||
@ -33,14 +33,6 @@ if (config.nazhua.nezhaVersion) {
|
|||||||
config.init = true;
|
config.init = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function handle$$serverStatus() {
|
|
||||||
if (window.$$serverStatus) {
|
|
||||||
config.nazhua.listServerItemType = 'server-status';
|
|
||||||
config.nazhua.homeWorldMapPosition = 'bottom';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
handle$$serverStatus();
|
|
||||||
|
|
||||||
function setColorMode() {
|
function setColorMode() {
|
||||||
if (config.nazhua.simpleColorMode) {
|
if (config.nazhua.simpleColorMode) {
|
||||||
document.body.classList.add('simple-color-mode');
|
document.body.classList.add('simple-color-mode');
|
||||||
@ -72,7 +64,6 @@ export function mergeNazhuaConfig(customConfig) {
|
|||||||
});
|
});
|
||||||
replaceFavicon();
|
replaceFavicon();
|
||||||
setColorMode();
|
setColorMode();
|
||||||
handle$$serverStatus();
|
|
||||||
}
|
}
|
||||||
// 暴露合并配置方法
|
// 暴露合并配置方法
|
||||||
window.$mergeNazhuaConfig = mergeNazhuaConfig;
|
window.$mergeNazhuaConfig = mergeNazhuaConfig;
|
||||||
|
|||||||
@ -77,12 +77,6 @@ const codeMaps = {
|
|||||||
name: '吉隆坡',
|
name: '吉隆坡',
|
||||||
country: '马来西亚',
|
country: '马来西亚',
|
||||||
},
|
},
|
||||||
BKK: {
|
|
||||||
name: '曼谷',
|
|
||||||
country: '泰国',
|
|
||||||
x: 985,
|
|
||||||
y: 296,
|
|
||||||
},
|
|
||||||
HAN: {
|
HAN: {
|
||||||
x: 998,
|
x: 998,
|
||||||
y: 274,
|
y: 274,
|
||||||
@ -95,24 +89,6 @@ const codeMaps = {
|
|||||||
name: '胡志明市',
|
name: '胡志明市',
|
||||||
country: '越南',
|
country: '越南',
|
||||||
},
|
},
|
||||||
BOM: {
|
|
||||||
name: '孟买',
|
|
||||||
country: '印度',
|
|
||||||
x: 874,
|
|
||||||
y: 284,
|
|
||||||
},
|
|
||||||
DEL: {
|
|
||||||
name: '新德里',
|
|
||||||
country: '印度',
|
|
||||||
x: 886,
|
|
||||||
y: 246,
|
|
||||||
},
|
|
||||||
DXB: {
|
|
||||||
name: '迪拜',
|
|
||||||
country: '阿联酋',
|
|
||||||
x: 794.5,
|
|
||||||
y: 252,
|
|
||||||
},
|
|
||||||
LAX: {
|
LAX: {
|
||||||
x: 95,
|
x: 95,
|
||||||
y: 207,
|
y: 207,
|
||||||
@ -161,12 +137,6 @@ const codeMaps = {
|
|||||||
name: '纽约',
|
name: '纽约',
|
||||||
country: '美国',
|
country: '美国',
|
||||||
},
|
},
|
||||||
IAD: {
|
|
||||||
name: '阿什本',
|
|
||||||
country: 'US',
|
|
||||||
x: 265,
|
|
||||||
y: 186,
|
|
||||||
},
|
|
||||||
DFW: {
|
DFW: {
|
||||||
x: 172,
|
x: 172,
|
||||||
y: 211,
|
y: 211,
|
||||||
@ -281,30 +251,6 @@ const codeMaps = {
|
|||||||
name: '布加勒斯特',
|
name: '布加勒斯特',
|
||||||
country: '罗马尼亚',
|
country: '罗马尼亚',
|
||||||
},
|
},
|
||||||
SOF: {
|
|
||||||
name: '索菲亚',
|
|
||||||
country: '保加利亚',
|
|
||||||
x: 662.5,
|
|
||||||
y: 167,
|
|
||||||
},
|
|
||||||
VNO: {
|
|
||||||
name: '维尔纽斯',
|
|
||||||
country: '立陶宛',
|
|
||||||
x: 657.5,
|
|
||||||
y: 110.5,
|
|
||||||
},
|
|
||||||
OSL: {
|
|
||||||
name: '奥斯陆',
|
|
||||||
country: '挪威',
|
|
||||||
x: 615.5,
|
|
||||||
y: 93,
|
|
||||||
},
|
|
||||||
RBA: {
|
|
||||||
name: '拉巴特',
|
|
||||||
country: '摩洛哥',
|
|
||||||
x: 545,
|
|
||||||
y: 212,
|
|
||||||
},
|
|
||||||
IST: {
|
IST: {
|
||||||
x: 676,
|
x: 676,
|
||||||
y: 176,
|
y: 176,
|
||||||
@ -329,7 +275,6 @@ export const aliasMapping = {
|
|||||||
HK: 'HKG',
|
HK: 'HKG',
|
||||||
MO: 'MFM',
|
MO: 'MFM',
|
||||||
TW: 'TPE',
|
TW: 'TPE',
|
||||||
ASH: 'IAD',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const countryCodeMapping = {
|
export const countryCodeMapping = {
|
||||||
@ -339,9 +284,6 @@ export const countryCodeMapping = {
|
|||||||
KR: 'SEL',
|
KR: 'SEL',
|
||||||
MY: 'KUL',
|
MY: 'KUL',
|
||||||
VN: 'HAN',
|
VN: 'HAN',
|
||||||
IN: 'DEL',
|
|
||||||
TH: 'BKK',
|
|
||||||
AE: 'DXB',
|
|
||||||
TR: 'IST',
|
TR: 'IST',
|
||||||
RO: 'OTP',
|
RO: 'OTP',
|
||||||
LU: 'LUX',
|
LU: 'LUX',
|
||||||
@ -360,10 +302,6 @@ export const countryCodeMapping = {
|
|||||||
IT: 'MXP',
|
IT: 'MXP',
|
||||||
ES: 'MAD',
|
ES: 'MAD',
|
||||||
PL: 'WAW',
|
PL: 'WAW',
|
||||||
BG: 'SOF',
|
|
||||||
LT: 'VNO',
|
|
||||||
NO: 'OSL',
|
|
||||||
MA: 'RBA',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default codeMaps;
|
export default codeMaps;
|
||||||
|
|||||||
@ -54,7 +54,6 @@ const store = useStore();
|
|||||||
const footerSlogan = computed(() => decodeURIComponent(config.nazhua?.footerSlogan || ''));
|
const footerSlogan = computed(() => decodeURIComponent(config.nazhua?.footerSlogan || ''));
|
||||||
|
|
||||||
const dynamicContentRef = ref();
|
const dynamicContentRef = ref();
|
||||||
const executedScripts = ref(new Set()); // 记录已执行的脚本,避免重复执行
|
|
||||||
|
|
||||||
const dynamicContent = computed(() => {
|
const dynamicContent = computed(() => {
|
||||||
if (store.state.setting?.config?.custom_code) {
|
if (store.state.setting?.config?.custom_code) {
|
||||||
@ -70,69 +69,24 @@ const dynamicContent = computed(() => {
|
|||||||
const executeScripts = () => {
|
const executeScripts = () => {
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
if (!dynamicContentRef.value) return;
|
if (!dynamicContentRef.value) return;
|
||||||
|
|
||||||
const scripts = dynamicContentRef.value.querySelectorAll('script');
|
const scripts = dynamicContentRef.value.querySelectorAll('script');
|
||||||
|
|
||||||
scripts.forEach((script) => {
|
scripts.forEach((script) => {
|
||||||
try {
|
const newScript = document.createElement('script');
|
||||||
// 生成脚本唯一标识,避免重复执行
|
newScript.type = 'text/javascript';
|
||||||
const scriptIdentifier = script.src || script.textContent || '';
|
if (script.src) {
|
||||||
if (!scriptIdentifier || executedScripts.value.has(scriptIdentifier)) {
|
newScript.src = script.src; // 拷贝外部脚本的 src
|
||||||
return;
|
} else {
|
||||||
}
|
newScript.textContent = script.textContent; // 拷贝内联脚本
|
||||||
|
|
||||||
const newScript = document.createElement('script');
|
|
||||||
newScript.type = script.type || 'text/javascript';
|
|
||||||
|
|
||||||
// 复制所有相关属性
|
|
||||||
if (script.async !== undefined) newScript.async = script.async;
|
|
||||||
if (script.defer !== undefined) newScript.defer = script.defer;
|
|
||||||
if (script.crossOrigin) newScript.crossOrigin = script.crossOrigin;
|
|
||||||
if (script.integrity) newScript.integrity = script.integrity;
|
|
||||||
if (script.noModule !== undefined) newScript.noModule = script.noModule;
|
|
||||||
if (script.referrerPolicy) newScript.referrerPolicy = script.referrerPolicy;
|
|
||||||
|
|
||||||
if (script.src) {
|
|
||||||
// 外部脚本:监听加载完成事件
|
|
||||||
newScript.src = script.src;
|
|
||||||
newScript.onload = () => {
|
|
||||||
executedScripts.value.add(scriptIdentifier);
|
|
||||||
};
|
|
||||||
newScript.onerror = (error) => {
|
|
||||||
console.error('Failed to load external script:', script.src, error);
|
|
||||||
};
|
|
||||||
document.body.appendChild(newScript);
|
|
||||||
} else {
|
|
||||||
// 内联脚本:直接执行
|
|
||||||
newScript.textContent = script.textContent;
|
|
||||||
document.body.appendChild(newScript);
|
|
||||||
executedScripts.value.add(scriptIdentifier);
|
|
||||||
// 内联脚本执行后可以安全移除
|
|
||||||
document.body.removeChild(newScript);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error executing dynamic script:', error);
|
|
||||||
}
|
}
|
||||||
|
document.body.appendChild(newScript);
|
||||||
|
document.body.removeChild(newScript); // 可选:移除以保持整洁
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// 清理已执行脚本的记录(当内容变化时)
|
watch(dynamicContent, () => {
|
||||||
const cleanupScripts = () => {
|
if (dynamicContent.value) {
|
||||||
executedScripts.value.clear();
|
executeScripts();
|
||||||
};
|
|
||||||
|
|
||||||
watch(dynamicContent, (newVal, oldVal) => {
|
|
||||||
// 内容变化时,清理旧的执行记录
|
|
||||||
if (newVal !== oldVal) {
|
|
||||||
cleanupScripts();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newVal) {
|
|
||||||
// 确保 DOM 已更新
|
|
||||||
nextTick(() => {
|
|
||||||
executeScripts();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -187,10 +187,6 @@ onMounted(() => {
|
|||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
window.removeEventListener('keydown', handleKeyDown);
|
window.removeEventListener('keydown', handleKeyDown);
|
||||||
window.removeEventListener('keydown', handleEscKey);
|
window.removeEventListener('keydown', handleEscKey);
|
||||||
if (handleSearchTimer) {
|
|
||||||
clearTimeout(handleSearchTimer);
|
|
||||||
handleSearchTimer = null;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@ -32,7 +32,6 @@
|
|||||||
import {
|
import {
|
||||||
ref,
|
ref,
|
||||||
computed,
|
computed,
|
||||||
onUnmounted,
|
|
||||||
} from 'vue';
|
} from 'vue';
|
||||||
import config from '@/config';
|
import config from '@/config';
|
||||||
import Fireworks from '@/components/fireworks.vue';
|
import Fireworks from '@/components/fireworks.vue';
|
||||||
@ -74,14 +73,8 @@ const enableInnerSearch = computed(() => {
|
|||||||
return config.nazhua.enableInnerSearch;
|
return config.nazhua.enableInnerSearch;
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleResize = () => {
|
window.addEventListener('resize', () => {
|
||||||
windowWidth.value = window.innerWidth;
|
windowWidth.value = window.innerWidth;
|
||||||
};
|
|
||||||
|
|
||||||
window.addEventListener('resize', handleResize);
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
window.removeEventListener('resize', handleResize);
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@ -30,7 +30,7 @@ function useCdnCss(item) {
|
|||||||
if (import.meta.env.VITE_USE_CDN) {
|
if (import.meta.env.VITE_USE_CDN) {
|
||||||
Object.entries({
|
Object.entries({
|
||||||
remixicon: {
|
remixicon: {
|
||||||
jsdelivr: 'https://cdn.jsdelivr.net/npm/remixicon@4.7.0/fonts/remixicon.css',
|
jsdelivr: 'https://cdn.jsdelivr.net/npm/remixicon@4.6.0/fonts/remixicon.css',
|
||||||
cdnjs: 'https://cdnjs.cloudflare.com/ajax/libs/remixicon/4.2.0/remixicon.css',
|
cdnjs: 'https://cdnjs.cloudflare.com/ajax/libs/remixicon/4.2.0/remixicon.css',
|
||||||
},
|
},
|
||||||
flagIcons: {
|
flagIcons: {
|
||||||
|
|||||||
@ -35,13 +35,7 @@ export default async () => fetch(getNezhaConfigUrl()).then((res) => res.text()).
|
|||||||
if (!configStr) {
|
if (!configStr) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
let remoteConfig;
|
const remoteConfig = JSON.parse(unescaped(configStr));
|
||||||
try {
|
|
||||||
remoteConfig = JSON.parse(unescaped(configStr));
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to parse nezha config:', error);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (remoteConfig?.servers) {
|
if (remoteConfig?.servers) {
|
||||||
remoteConfig.servers = remoteConfig.servers.map((i) => {
|
remoteConfig.servers = remoteConfig.servers.map((i) => {
|
||||||
const item = {
|
const item = {
|
||||||
@ -49,8 +43,7 @@ export default async () => fetch(getNezhaConfigUrl()).then((res) => res.text()).
|
|||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
item.PublicNote = JSON.parse(i.PublicNote);
|
item.PublicNote = JSON.parse(i.PublicNote);
|
||||||
} catch (error) {
|
} catch {
|
||||||
console.warn('Failed to parse PublicNote for server:', i.ID || i.id, error);
|
|
||||||
item.PublicNote = {};
|
item.PublicNote = {};
|
||||||
}
|
}
|
||||||
return item;
|
return item;
|
||||||
@ -58,10 +51,7 @@ export default async () => fetch(getNezhaConfigUrl()).then((res) => res.text()).
|
|||||||
return remoteConfig;
|
return remoteConfig;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}).catch((error) => {
|
}).catch(() => null);
|
||||||
console.error('Failed to load nezha config:', error);
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取标签列表
|
* 获取标签列表
|
||||||
|
|||||||
@ -21,10 +21,7 @@ export const loadServerGroup = async () => request({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}).catch((error) => {
|
}).catch(() => null);
|
||||||
console.error('Failed to load server group:', error);
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 加载网站配置
|
* 加载网站配置
|
||||||
@ -40,10 +37,7 @@ export const loadSetting = async () => request({
|
|||||||
return res.data?.data || {};
|
return res.data?.data || {};
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}).catch((error) => {
|
}).catch(() => null);
|
||||||
console.error('Failed to load setting:', error);
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 加载个人信息
|
* 加载个人信息
|
||||||
@ -59,7 +53,4 @@ export const loadProfile = async (check) => request({
|
|||||||
return res.data?.data || {};
|
return res.data?.data || {};
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}).catch((error) => {
|
}).catch(() => null);
|
||||||
console.error('Failed to load profile:', error);
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
|
|||||||
@ -122,11 +122,10 @@ export default function (v1Data) {
|
|||||||
try {
|
try {
|
||||||
v0Data.PublicNote = JSON.parse(v1Data.public_note);
|
v0Data.PublicNote = JSON.parse(v1Data.public_note);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('Failed to parse public_note for server:', v1Data.id, e);
|
v1Data.PublicNote = null;
|
||||||
v0Data.PublicNote = null;
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
v0Data.PublicNote = null;
|
v1Data.PublicNote = null;
|
||||||
}
|
}
|
||||||
return v0Data;
|
return v0Data;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -261,9 +261,7 @@ const monitorChartType = computed(() => {
|
|||||||
|
|
||||||
// 服务器时间(后面来自接口)
|
// 服务器时间(后面来自接口)
|
||||||
const nowServerTime = computed(() => store.state.serverTime || Date.now());
|
const nowServerTime = computed(() => store.state.serverTime || Date.now());
|
||||||
// const nowServerTime = computed(() => Date.now());
|
const accpetShowTime = computed(() => (Math.floor(nowServerTime.value / 60000) - minute.value) * 60000);
|
||||||
// console.log(store.state.serverTime);
|
|
||||||
const acceptShowTime = 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);
|
||||||
@ -282,162 +280,112 @@ const monitorChartData = computed(() => {
|
|||||||
* - valueList {Array}: 包含以下内容的对象列表:
|
* - valueList {Array}: 包含以下内容的对象列表:
|
||||||
* - name {String}: 监控名称。
|
* - name {String}: 监控名称。
|
||||||
* - data {Array}: [时间戳, 平均延迟] 对的数组。
|
* - data {Array}: [时间戳, 平均延迟] 对的数组。
|
||||||
|
*
|
||||||
|
* 该函数执行以下步骤:
|
||||||
|
* 1. 遍历监控数据以分类和过滤平均延迟。
|
||||||
|
* 2. 如果启用了削峰,则应用削峰以过滤异常值。
|
||||||
|
* 3. 构建监控名称到其各自时间戳和平均延迟的映射。
|
||||||
|
* 4. 将映射转换为监控名称、时间戳和平均延迟数据的列表。
|
||||||
|
* 5. 删除重复的时间戳并对其进行排序。
|
||||||
*/
|
*/
|
||||||
const cateList = [];
|
|
||||||
const cateMap = {};
|
const cateMap = {};
|
||||||
const dateSet = new Set();
|
|
||||||
let valueList = [];
|
|
||||||
monitorData.value.forEach((i) => {
|
monitorData.value.forEach((i) => {
|
||||||
const dateMap = new Map();
|
const dateMap = {};
|
||||||
const {
|
if (!cateMap[i.monitor_name]) {
|
||||||
monitor_name,
|
cateMap[i.monitor_name] = {
|
||||||
monitor_id,
|
id: i.monitor_id,
|
||||||
created_at,
|
dateMap,
|
||||||
avg_delay,
|
avgs: [],
|
||||||
} = i;
|
|
||||||
if (!cateMap[monitor_name]) {
|
|
||||||
cateMap[monitor_name] = {
|
|
||||||
id: monitor_id,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const cateDelayMap = new Map();
|
const showAvgDelay = [];
|
||||||
const cateAcceptTimeMap = new Map();
|
const showCreateTime = [];
|
||||||
const cateCreateTime = new Set();
|
const accpeTimeMap = {};
|
||||||
|
i.created_at.forEach((o, index) => {
|
||||||
// 实际数据的最早时间戳
|
const status = o >= accpetShowTime.value;
|
||||||
let earliestTimestamp = nowServerTime.value;
|
|
||||||
created_at.forEach((time, index) => {
|
|
||||||
if (time < earliestTimestamp) {
|
|
||||||
earliestTimestamp = time;
|
|
||||||
}
|
|
||||||
const status = time >= acceptShowTime.value;
|
|
||||||
|
|
||||||
// 允许显示的数据,记录到cateAcceptTime
|
|
||||||
if (status) {
|
if (status) {
|
||||||
if (import.meta.env.VITE_MONITOR_DEBUG === '1' && cateAcceptTimeMap.has(time)) {
|
accpeTimeMap[o] = i.avg_delay[index];
|
||||||
console.log(`${monitor_name} ${time} 重复,值对比: ${avg_delay[index]} vs ${cateAcceptTimeMap.get(time)}`);
|
|
||||||
}
|
|
||||||
cateAcceptTimeMap.set(time, avg_delay[index]);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (import.meta.env.VITE_MONITOR_DEBUG === '1') {
|
const allMintues = Math.floor((Date.now() - accpetShowTime.value) / 60000);
|
||||||
console.log(`${monitor_name} created_at`, earliestTimestamp);
|
|
||||||
console.log(`${monitor_name} created_at`, JSON.parse(JSON.stringify(created_at)));
|
|
||||||
console.log(`${monitor_name} avg_delay`, JSON.parse(JSON.stringify(avg_delay)));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 允许显示的最早时间戳,用于生成显示时间范围内的数据
|
|
||||||
const actualStartTime = Math.max(
|
|
||||||
acceptShowTime.value,
|
|
||||||
earliestTimestamp,
|
|
||||||
);
|
|
||||||
|
|
||||||
// 显示时间范围内的分钟数
|
|
||||||
const allMintues = Math.floor((Date.now() - actualStartTime) / 60000);
|
|
||||||
|
|
||||||
// 合成分钟数据
|
|
||||||
for (let j = 0; j < allMintues; j += 1) {
|
for (let j = 0; j < allMintues; j += 1) {
|
||||||
const time = actualStartTime + j * 60000;
|
const time = accpetShowTime.value + j * 60000;
|
||||||
// 记录创建时间
|
showCreateTime.push(time);
|
||||||
cateCreateTime.add(time);
|
const timeProp = accpeTimeMap[time];
|
||||||
// 记录延迟数据
|
if (timeProp) {
|
||||||
const timeProp = cateAcceptTimeMap.get(time);
|
showAvgDelay.push(timeProp);
|
||||||
cateDelayMap.set(time, timeProp ?? undefined);
|
} else {
|
||||||
|
showAvgDelay.push(null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 计算削峰阈值
|
|
||||||
const {
|
const {
|
||||||
median,
|
threshold,
|
||||||
tolerancePercent,
|
mean,
|
||||||
} = peakShaving.value ? getThreshold(Array.from(cateDelayMap.values())) : {};
|
max,
|
||||||
|
min,
|
||||||
// 合成分钟数据
|
} = peakShaving.value ? getThreshold(showAvgDelay, 2) : {};
|
||||||
cateCreateTime.values().forEach((time) => {
|
showCreateTime.forEach((o, index) => {
|
||||||
const avgDelay = cateDelayMap.get(time) * 1;
|
if (Object.prototype.hasOwnProperty.call(dateMap, o)) {
|
||||||
|
return;
|
||||||
// 只对有效的延迟值进行削峰判断
|
}
|
||||||
|
const avgDelay = showAvgDelay[index];
|
||||||
if (peakShaving.value) {
|
if (peakShaving.value) {
|
||||||
// 削峰过滤:根据中位数和动态容差百分比判断异常值
|
if (avgDelay === 0) {
|
||||||
const threshold = median * tolerancePercent;
|
dateMap[o] = null;
|
||||||
// 当偏离中位数超过阈值时,视为异常值
|
return;
|
||||||
if (Math.abs(avgDelay - median) > threshold) {
|
}
|
||||||
dateMap.set(time, null);
|
if (Math.abs(avgDelay - mean) > threshold && max / min > 2) {
|
||||||
|
dateMap[o] = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 无数据或无效数据的情况,设置为undefined
|
dateMap[o] = avgDelay ? (avgDelay).toFixed(2) * 1 : null;
|
||||||
if (Number.isNaN(avgDelay)) {
|
|
||||||
dateMap.set(time, undefined);
|
|
||||||
} else {
|
|
||||||
dateMap.set(time, (avgDelay).toFixed(2) * 1);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
});
|
||||||
const lineData = [];
|
let dateList = [];
|
||||||
const validatedData = [];
|
let valueList = [];
|
||||||
const overValidatedData = [];
|
const cateList = [];
|
||||||
let delayTotal = 0;
|
Object.keys(cateMap).forEach((i) => {
|
||||||
dateMap.forEach((val, key) => {
|
const {
|
||||||
const time = parseInt(key, 10); // 时间戳
|
id,
|
||||||
lineData.push([time, val || null]);
|
dateMap,
|
||||||
if (val) {
|
avgs,
|
||||||
dateSet.add(time);
|
} = cateMap[i];
|
||||||
validatedData.push([time, val]);
|
Object.entries(dateMap).forEach(([key, value]) => {
|
||||||
delayTotal += val;
|
const time = parseInt(key, 10);
|
||||||
}
|
avgs.push([time, value]);
|
||||||
if (val !== undefined) {
|
dateList.push(time);
|
||||||
overValidatedData.push([time, val]);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
const color = getLineColor(id);
|
||||||
if (import.meta.env.VITE_MONITOR_DEBUG === '1') {
|
if (avgs.length) {
|
||||||
cateMap[monitor_name].origin = {
|
|
||||||
cateCreateTime,
|
|
||||||
cateDelayMap,
|
|
||||||
cateAcceptTimeMap,
|
|
||||||
dateMap,
|
|
||||||
lineData,
|
|
||||||
validatedData,
|
|
||||||
overValidatedData,
|
|
||||||
delayTotal,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const id = monitor_id;
|
|
||||||
// 计算平均延迟
|
|
||||||
const avgDelay = delayTotal / validatedData.length || 0;
|
|
||||||
|
|
||||||
if (lineData && lineData.length) {
|
|
||||||
if (!validate.hasOwn(showCates.value, id)) {
|
if (!validate.hasOwn(showCates.value, id)) {
|
||||||
showCates.value[id] = true;
|
showCates.value[id] = true;
|
||||||
}
|
}
|
||||||
const color = getLineColor(id);
|
// 计算平均延迟和成功率
|
||||||
// 成功率 = 有效数据点 / 所有数据点
|
const validAvgs = avgs.filter((a) => a[1] !== 0 && a[1] !== null);
|
||||||
const over = overValidatedData.length > 0 ? overValidatedData.length / lineData.length : 0;
|
const avg = validAvgs.reduce((a, b) => a + b[1], 0) / validAvgs.length;
|
||||||
const validRate = 1 - ((validatedData.length > 0 && overValidatedData.length > 0)
|
const over = avgs.filter((a) => a[1] !== 0 && a[1] !== null).length / avgs.length;
|
||||||
? validatedData.length / overValidatedData.length : 0);
|
|
||||||
const cateItem = {
|
const cateItem = {
|
||||||
id,
|
id,
|
||||||
name: monitor_name,
|
name: i,
|
||||||
color,
|
color,
|
||||||
avg: avgDelay.toFixed(2) * 1,
|
avg: avg.toFixed(2) * 1,
|
||||||
over: (over * 100).toFixed(2) * 1,
|
over: (over * 100).toFixed(2) * 1,
|
||||||
validRate: (validRate * 100).toFixed(2) * 1,
|
|
||||||
};
|
};
|
||||||
|
if (Number.isNaN(cateItem.avg)) {
|
||||||
|
cateItem.avg = 0;
|
||||||
|
}
|
||||||
const titles = [
|
const titles = [
|
||||||
cateItem.name,
|
cateItem.name,
|
||||||
cateItem.avg === 0 ? '' : `平均延迟:${cateItem.avg}ms`,
|
cateItem.avg === 0 ? '' : `平均延迟:${cateItem.avg}ms`,
|
||||||
`成功率:${cateItem.over}%`,
|
`成功率:${cateItem.over}%`,
|
||||||
];
|
];
|
||||||
if (peakShaving.value) {
|
|
||||||
titles.push(`削峰率: ${cateItem.validRate}%`);
|
|
||||||
}
|
|
||||||
cateItem.title = titles.filter((s) => s).join('\n');
|
cateItem.title = titles.filter((s) => s).join('\n');
|
||||||
cateList.push(cateItem);
|
cateList.push(cateItem);
|
||||||
valueList.push({
|
valueList.push({
|
||||||
id,
|
id,
|
||||||
name: monitor_name,
|
name: i,
|
||||||
data: lineData,
|
data: avgs,
|
||||||
itemStyle: {
|
itemStyle: {
|
||||||
color,
|
color,
|
||||||
},
|
},
|
||||||
@ -447,15 +395,8 @@ const monitorChartData = computed(() => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
dateList = dateList.sort((a, b) => a - b);
|
||||||
const dateList = Array.from(dateSet).sort((a, b) => a - b);
|
|
||||||
valueList = valueList.filter((i) => showCates.value[i.id]);
|
valueList = valueList.filter((i) => showCates.value[i.id]);
|
||||||
|
|
||||||
if (import.meta.env.VITE_MONITOR_DEBUG === '1') {
|
|
||||||
window._cateMap = cateMap;
|
|
||||||
console.log(window._cateMap);
|
|
||||||
console.log(dateList, cateList, valueList);
|
|
||||||
}
|
|
||||||
return {
|
return {
|
||||||
dateList,
|
dateList,
|
||||||
cateList,
|
cateList,
|
||||||
@ -627,12 +568,6 @@ onUnmounted(() => {
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
|
|
||||||
@media screen and (min-width: 768px) {
|
|
||||||
position: sticky;
|
|
||||||
top: var(--layout-header-height);
|
|
||||||
z-index: 1000;
|
|
||||||
}
|
|
||||||
|
|
||||||
.module-title {
|
.module-title {
|
||||||
width: max-content;
|
width: max-content;
|
||||||
height: 30px;
|
height: 30px;
|
||||||
|
|||||||
@ -162,24 +162,12 @@ const show = computed(() => {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
|
height: 40px;
|
||||||
border-bottom-left-radius: var(--list-item-border-radius);
|
border-bottom-left-radius: var(--list-item-border-radius);
|
||||||
border-bottom-right-radius: var(--list-item-border-radius);
|
border-bottom-right-radius: var(--list-item-border-radius);
|
||||||
background: rgba(#000, 0.3);
|
background: rgba(#000, 0.3);
|
||||||
box-shadow: 0 -2px 4px rgba(#000, 0.5);
|
box-shadow: 0 -2px 4px rgba(#000, 0.5);
|
||||||
|
|
||||||
--list-item-bill-height: 40px;
|
|
||||||
--list-item-bill-font-size: 14px;
|
|
||||||
--list-item-bill-icon-font-size: 16px;
|
|
||||||
|
|
||||||
height: var(--list-item-bill-height);
|
|
||||||
font-size: var(--list-item-bill-font-size);
|
|
||||||
|
|
||||||
@media screen and (max-width: 720px) {
|
|
||||||
--list-item-bill-height: 30px;
|
|
||||||
--list-item-bill-font-size: 12px;
|
|
||||||
--list-item-bill-icon-font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.dot-dot-box--hide {
|
&.dot-dot-box--hide {
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
border-top: 1px solid rgba(#ddd, 0.1);
|
border-top: 1px solid rgba(#ddd, 0.1);
|
||||||
@ -198,27 +186,23 @@ const show = computed(() => {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: calc(var(--list-item-bill-height) * 0.75);
|
width: 30px;
|
||||||
height: calc(var(--list-item-bill-height) * 0.75);
|
height: 30px;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
font-size: var(--list-item-bill-icon-font-size);
|
font-size: 16px;
|
||||||
color: #74dbef;
|
color: #74dbef;
|
||||||
}
|
}
|
||||||
|
|
||||||
.text {
|
.text {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
line-height: var(--list-item-bill-height);
|
line-height: 30px;
|
||||||
color: #ddd;
|
color: #ddd;
|
||||||
}
|
}
|
||||||
|
|
||||||
.value-text {
|
.value-text {
|
||||||
color: #74dbef;
|
color: #74dbef;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 720px) {
|
|
||||||
padding-left: 6px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.tag-list {
|
.tag-list {
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="server-list-item-status"
|
class="server-list-item-status"
|
||||||
:class="classNames"
|
:class="'type--' + componentName"
|
||||||
>
|
>
|
||||||
<component
|
<component
|
||||||
:is="componentMaps[componentName]"
|
:is="componentMaps[componentName]"
|
||||||
@ -21,10 +21,6 @@
|
|||||||
* 服务器状态盒子
|
* 服务器状态盒子
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {
|
|
||||||
computed,
|
|
||||||
} from 'vue';
|
|
||||||
|
|
||||||
import config from '@/config';
|
import config from '@/config';
|
||||||
|
|
||||||
import handleServerStatus from '@/views/composable/server-status';
|
import handleServerStatus from '@/views/composable/server-status';
|
||||||
@ -43,13 +39,10 @@ const componentMaps = {
|
|||||||
progress: ServerStatusProgress,
|
progress: ServerStatusProgress,
|
||||||
};
|
};
|
||||||
|
|
||||||
const componentName = computed(() => {
|
const componentName = [
|
||||||
const name = [
|
'donut',
|
||||||
'donut',
|
'progress',
|
||||||
'progress',
|
].includes(config.nazhua.listServerStatusType) ? config.nazhua.listServerStatusType : 'donut';
|
||||||
].includes(config.nazhua.listServerStatusType) ? config.nazhua.listServerStatusType : 'donut';
|
|
||||||
return config.nazhua.listServerItemType === 'server-status' ? 'progress' : name;
|
|
||||||
});
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
serverStatusList,
|
serverStatusList,
|
||||||
@ -58,13 +51,6 @@ const {
|
|||||||
statusListTpl: 'cpu,mem,disk',
|
statusListTpl: 'cpu,mem,disk',
|
||||||
statusListItemContent: false,
|
statusListItemContent: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const classNames = computed(() => {
|
|
||||||
const names = {};
|
|
||||||
names[`type--${componentName.value}`] = true;
|
|
||||||
names[`len--${serverStatusList.value?.length}`] = true;
|
|
||||||
return names;
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@ -77,16 +63,11 @@ const classNames = computed(() => {
|
|||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
|
|
||||||
--progress-bar-width: calc(50% - 5px);
|
|
||||||
--progress-bar-height: 20px;
|
--progress-bar-height: 20px;
|
||||||
|
|
||||||
@media screen and (max-width: 400px) {
|
@media screen and (max-width: 350px) {
|
||||||
--progress-bar-height: 16px;
|
--progress-bar-height: 16px;
|
||||||
padding: 0 10px;
|
padding: 0 15px;
|
||||||
}
|
|
||||||
|
|
||||||
&.len--3 {
|
|
||||||
--progress-bar-width: calc((100% - 20px) / 3);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -90,7 +90,7 @@ const { cpuAndMemAndDisk } = handleServerInfo({
|
|||||||
const platformLogoIconClassName = computed(() => hostUtils.getPlatformLogoIconClassName(props.info?.Host?.Platform));
|
const platformLogoIconClassName = computed(() => hostUtils.getPlatformLogoIconClassName(props.info?.Host?.Platform));
|
||||||
|
|
||||||
const serverRealTimeListTpls = computed(() => {
|
const serverRealTimeListTpls = computed(() => {
|
||||||
if (config.nazhua?.listServerRealTimeShowLoad || config.nazhua.listServerItemType === 'server-status') {
|
if (config.nazhua?.listServerRealTimeShowLoad) {
|
||||||
return 'D-A-T,T-A-U,L-A-P,I-A-O';
|
return 'D-A-T,T-A-U,L-A-P,I-A-O';
|
||||||
}
|
}
|
||||||
return 'duration,transfer,inSpeed,outSpeed';
|
return 'duration,transfer,inSpeed,outSpeed';
|
||||||
@ -114,12 +114,12 @@ function openDetail() {
|
|||||||
transition: 0.3s;
|
transition: 0.3s;
|
||||||
|
|
||||||
.server-info-group {
|
.server-info-group {
|
||||||
--list-item-head-height: 50px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
padding: 0 15px;
|
padding: 0 15px;
|
||||||
|
height: 50px;
|
||||||
border-top-left-radius: var(--list-item-border-radius);
|
border-top-left-radius: var(--list-item-border-radius);
|
||||||
border-top-right-radius: var(--list-item-border-radius);
|
border-top-right-radius: var(--list-item-border-radius);
|
||||||
background: rgba(#000, 0.3);
|
background: rgba(#000, 0.3);
|
||||||
@ -128,7 +128,6 @@ function openDetail() {
|
|||||||
|
|
||||||
@media screen and (max-width: 768px) {
|
@media screen and (max-width: 768px) {
|
||||||
cursor: default;
|
cursor: default;
|
||||||
--list-item-head-height: 40px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&.dot-dot-box--hide {
|
&.dot-dot-box--hide {
|
||||||
@ -139,7 +138,6 @@ function openDetail() {
|
|||||||
&.server-list-item-head {
|
&.server-list-item-head {
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
height: var(--list-item-head-height, 50px);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.left-box,
|
.left-box,
|
||||||
@ -198,8 +196,6 @@ function openDetail() {
|
|||||||
--real-time-text-font-size: 12px;
|
--real-time-text-font-size: 12px;
|
||||||
--real-time-label-font-size: 14px;
|
--real-time-label-font-size: 14px;
|
||||||
|
|
||||||
font-size: var(--real-time-label-font-size);
|
|
||||||
|
|
||||||
@media screen and (max-width: 1280px) {
|
@media screen and (max-width: 1280px) {
|
||||||
padding: 10px 0 15px;
|
padding: 10px 0 15px;
|
||||||
|
|
||||||
@ -214,12 +210,8 @@ function openDetail() {
|
|||||||
--real-time-value-font-size: 20px;
|
--real-time-value-font-size: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 720px) {
|
@media screen and (max-width: 680px) {
|
||||||
--real-time-value-font-size: 24px;
|
--real-time-value-font-size: 24px;
|
||||||
--real-time-text-font-size: 12px;
|
|
||||||
--real-time-label-font-size: 12px;
|
|
||||||
|
|
||||||
padding: 5px 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 320px) {
|
@media screen and (max-width: 320px) {
|
||||||
|
|||||||
@ -7,7 +7,6 @@
|
|||||||
:class="{
|
:class="{
|
||||||
'server-list--row': showListRow,
|
'server-list--row': showListRow,
|
||||||
'server-list--card': showListCard,
|
'server-list--card': showListCard,
|
||||||
'server-list--status': showListByServerStatus,
|
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<slot />
|
<slot />
|
||||||
@ -18,7 +17,6 @@
|
|||||||
:class="{
|
:class="{
|
||||||
'server-list--row': showListRow,
|
'server-list--row': showListRow,
|
||||||
'server-list--card': showListCard,
|
'server-list--card': showListCard,
|
||||||
'server-list--status': showListByServerStatus,
|
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<slot />
|
<slot />
|
||||||
@ -43,10 +41,6 @@ defineProps({
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
showListByServerStatus: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -100,18 +94,6 @@ defineProps({
|
|||||||
margin: auto;
|
margin: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.server-list-container.server-list--status {
|
|
||||||
--list-padding: 20px;
|
|
||||||
--list-gap-size: 12px;
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: var(--list-gap-size);
|
|
||||||
width: var(--list-container-width);
|
|
||||||
padding: 0 var(--list-padding);
|
|
||||||
margin: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.list-move,
|
.list-move,
|
||||||
.list-enter-active,
|
.list-enter-active,
|
||||||
.list-leave-active {
|
.list-leave-active {
|
||||||
|
|||||||
@ -49,7 +49,7 @@ const props = defineProps({
|
|||||||
type: Array,
|
type: Array,
|
||||||
default: () => [],
|
default: () => [],
|
||||||
},
|
},
|
||||||
acceptEmpty: {
|
accpetEmpty: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
@ -74,7 +74,7 @@ const activeValue = computed({
|
|||||||
|
|
||||||
function toggleModelValue(item) {
|
function toggleModelValue(item) {
|
||||||
if (activeValue.value === item.value) {
|
if (activeValue.value === item.value) {
|
||||||
if (props.acceptEmpty) {
|
if (props.accpetEmpty) {
|
||||||
activeValue.value = '';
|
activeValue.value = '';
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -1,341 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div
|
|
||||||
class="server-sort-box"
|
|
||||||
:class="{
|
|
||||||
'server-sort-box--light-background': lightBackground,
|
|
||||||
'server-sort-box--mobile-hide': !mobileShow,
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
ref="triggerRef"
|
|
||||||
class="sort-select-wrapper"
|
|
||||||
@click="toggleDropdown"
|
|
||||||
>
|
|
||||||
<div class="sort-select-selected">
|
|
||||||
<span class="sort-select-selected-value">{{ selectedLabel }}</span>
|
|
||||||
<span
|
|
||||||
class="sort-select-selected-icon"
|
|
||||||
@click.stop="toggleOrder"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
v-if="activeValue.order === 'desc'"
|
|
||||||
class="ri-arrow-down-line"
|
|
||||||
/>
|
|
||||||
<span
|
|
||||||
v-else
|
|
||||||
class="ri-arrow-up-line"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 下拉菜单 -->
|
|
||||||
<Teleport to="body">
|
|
||||||
<server-sort-dropdown-menu
|
|
||||||
ref="dropdownMenuRef"
|
|
||||||
:visible="isDropdownOpen"
|
|
||||||
:options="options"
|
|
||||||
:active-value="activeValue.prop"
|
|
||||||
:dropdown-style="dropdownStyle"
|
|
||||||
:light-background="lightBackground"
|
|
||||||
:is-mobile="isMobile"
|
|
||||||
@select="handleSelectItem"
|
|
||||||
/>
|
|
||||||
</Teleport>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
/**
|
|
||||||
* 过滤栏
|
|
||||||
*/
|
|
||||||
import {
|
|
||||||
computed,
|
|
||||||
ref,
|
|
||||||
onMounted,
|
|
||||||
onUnmounted,
|
|
||||||
nextTick,
|
|
||||||
} from 'vue';
|
|
||||||
import config from '@/config';
|
|
||||||
import ServerSortDropdownMenu from './server-sort-dropdown-menu.vue';
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
modelValue: {
|
|
||||||
type: Object,
|
|
||||||
default: () => ({
|
|
||||||
prop: 'DisplayIndex',
|
|
||||||
order: 'desc',
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
options: {
|
|
||||||
type: Array,
|
|
||||||
default: () => [],
|
|
||||||
},
|
|
||||||
acceptEmpty: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
mobileShow: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const emits = defineEmits([
|
|
||||||
'update:modelValue',
|
|
||||||
'change',
|
|
||||||
]);
|
|
||||||
|
|
||||||
const lightBackground = computed(() => config.nazhua.lightBackground);
|
|
||||||
|
|
||||||
// 设备检测(用于判断是否小屏,小屏时居中显示)
|
|
||||||
const isMobile = ref(window.innerWidth < 768);
|
|
||||||
|
|
||||||
// PC端下拉菜单相关
|
|
||||||
const isDropdownOpen = ref(false);
|
|
||||||
const triggerRef = ref(null);
|
|
||||||
const dropdownMenuRef = ref(null);
|
|
||||||
const dropdownStyle = ref({});
|
|
||||||
|
|
||||||
const activeValue = computed({
|
|
||||||
get: () => props.modelValue,
|
|
||||||
set: (val) => {
|
|
||||||
emits('update:modelValue', val);
|
|
||||||
emits('change', val);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// 获取当前选中项的label
|
|
||||||
const selectedLabel = computed(() => {
|
|
||||||
const selectedOption = props.options.find((opt) => opt.value === activeValue.value.prop);
|
|
||||||
return selectedOption ? selectedOption.label : '排序';
|
|
||||||
});
|
|
||||||
|
|
||||||
// 更新下拉菜单位置
|
|
||||||
function updateDropdownPosition() {
|
|
||||||
if (!triggerRef.value || !dropdownMenuRef.value) return;
|
|
||||||
|
|
||||||
// 使用 nextTick 确保 DOM 已更新
|
|
||||||
nextTick(() => {
|
|
||||||
const dropdownRef = dropdownMenuRef.value?.dropdownRef;
|
|
||||||
|
|
||||||
if (!dropdownRef) return;
|
|
||||||
|
|
||||||
// 小屏设备:居中显示
|
|
||||||
if (isMobile.value) {
|
|
||||||
dropdownStyle.value = {
|
|
||||||
position: 'fixed',
|
|
||||||
top: '50%',
|
|
||||||
left: '50%',
|
|
||||||
transform: 'translate(-50%, -50%)',
|
|
||||||
visibility: 'visible',
|
|
||||||
};
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 大屏设备:相对定位
|
|
||||||
const triggerRect = triggerRef.value.getBoundingClientRect();
|
|
||||||
|
|
||||||
// 先设置一个初始位置,确保元素在视口中可见
|
|
||||||
let top = triggerRect.bottom + 8;
|
|
||||||
let { left } = triggerRect;
|
|
||||||
|
|
||||||
// 设置初始位置
|
|
||||||
dropdownStyle.value = {
|
|
||||||
position: 'fixed',
|
|
||||||
top: `${top}px`,
|
|
||||||
left: `${left}px`,
|
|
||||||
visibility: 'hidden', // 先隐藏,避免闪烁
|
|
||||||
};
|
|
||||||
|
|
||||||
// 再次使用 nextTick 确保样式已应用
|
|
||||||
nextTick(() => {
|
|
||||||
const dropdownRect = dropdownRef.getBoundingClientRect();
|
|
||||||
|
|
||||||
// 防止超出右边界
|
|
||||||
if (left + dropdownRect.width > window.innerWidth) {
|
|
||||||
left = window.innerWidth - dropdownRect.width - 10;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 防止超出下边界,如果超出则向上展开
|
|
||||||
if (top + dropdownRect.height > window.innerHeight) {
|
|
||||||
top = triggerRect.top - dropdownRect.height - 8;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 防止超出左边界
|
|
||||||
if (left < 10) {
|
|
||||||
left = 10;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新最终位置并显示
|
|
||||||
dropdownStyle.value = {
|
|
||||||
position: 'fixed',
|
|
||||||
top: `${top}px`,
|
|
||||||
left: `${left}px`,
|
|
||||||
visibility: 'visible',
|
|
||||||
};
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 切换下拉菜单显示状态
|
|
||||||
function toggleDropdown(event) {
|
|
||||||
event.stopPropagation(); // 阻止事件冒泡,防止立即被 handleDocumentClick 关闭
|
|
||||||
isDropdownOpen.value = !isDropdownOpen.value;
|
|
||||||
if (isDropdownOpen.value) {
|
|
||||||
nextTick(() => {
|
|
||||||
updateDropdownPosition();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 切换升序/降序
|
|
||||||
function toggleOrder(event) {
|
|
||||||
event.stopPropagation(); // 阻止事件冒泡,避免触发下拉菜单
|
|
||||||
if (!activeValue.value.prop) return; // 如果没有选中排序字段,则不切换
|
|
||||||
|
|
||||||
activeValue.value = {
|
|
||||||
prop: activeValue.value.prop,
|
|
||||||
order: activeValue.value.order === 'desc' ? 'asc' : 'desc',
|
|
||||||
};
|
|
||||||
emits('change', activeValue.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
// PC端选择项
|
|
||||||
function handleSelectItem(item) {
|
|
||||||
if (activeValue.value.prop === item.value) {
|
|
||||||
if (props.acceptEmpty) {
|
|
||||||
activeValue.value = {
|
|
||||||
prop: '',
|
|
||||||
order: 'desc',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
activeValue.value = {
|
|
||||||
prop: item.value,
|
|
||||||
order: activeValue.value.order || 'desc',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
isDropdownOpen.value = false;
|
|
||||||
emits('change', activeValue.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 点击外部关闭下拉菜单
|
|
||||||
function handleDocumentClick(event) {
|
|
||||||
if (!isDropdownOpen.value) return;
|
|
||||||
|
|
||||||
const dropdownRef = dropdownMenuRef.value?.dropdownRef;
|
|
||||||
|
|
||||||
if (
|
|
||||||
triggerRef.value
|
|
||||||
&& !triggerRef.value.contains(event.target)
|
|
||||||
&& dropdownRef
|
|
||||||
&& !dropdownRef.contains(event.target)
|
|
||||||
) {
|
|
||||||
isDropdownOpen.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 窗口resize处理
|
|
||||||
function handleResize() {
|
|
||||||
isMobile.value = window.innerWidth < 768;
|
|
||||||
|
|
||||||
// 如果下拉菜单打开,更新位置
|
|
||||||
if (isDropdownOpen.value) {
|
|
||||||
nextTick(() => {
|
|
||||||
updateDropdownPosition();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
window.addEventListener('resize', handleResize);
|
|
||||||
document.addEventListener('click', handleDocumentClick);
|
|
||||||
});
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
window.removeEventListener('resize', handleResize);
|
|
||||||
document.removeEventListener('click', handleDocumentClick);
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.server-sort-box {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
padding: 0 var(--list-padding);
|
|
||||||
gap: 8px;
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
@media screen and (max-width: 768px) {
|
|
||||||
&--mobile-hide {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// PC端触发元素
|
|
||||||
.sort-select-wrapper {
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
@media screen and (min-width: 768px) {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.sort-select-selected {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
height: 36px;
|
|
||||||
padding: 0 15px;
|
|
||||||
line-height: 1.2;
|
|
||||||
border-radius: 6px;
|
|
||||||
background: rgba(#000, 0.3);
|
|
||||||
transition: all 0.3s linear;
|
|
||||||
|
|
||||||
@media screen and (min-width: 768px) {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (max-width: 768px) {
|
|
||||||
height: 30px;
|
|
||||||
padding: 0 10px;
|
|
||||||
border-radius: 3px;
|
|
||||||
background-color: rgba(#000, 0.8);
|
|
||||||
}
|
|
||||||
|
|
||||||
.sort-select-selected-value {
|
|
||||||
color: #fff;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sort-select-selected-icon {
|
|
||||||
margin-left: 8px;
|
|
||||||
color: #fff;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 2px 4px;
|
|
||||||
border-radius: 3px;
|
|
||||||
transition: all 0.2s linear;
|
|
||||||
|
|
||||||
@media screen and (min-width: 768px) {
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: rgba(#fff, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:active {
|
|
||||||
background: rgba(#fff, 0.2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// PC端浅色背景样式
|
|
||||||
&--light-background {
|
|
||||||
.sort-select-selected {
|
|
||||||
background: rgba(#000, 0.5);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -1,145 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div
|
|
||||||
v-show="visible"
|
|
||||||
ref="dropdownRef"
|
|
||||||
class="server-sort-select-dropdown"
|
|
||||||
:class="{
|
|
||||||
'server-sort-select-dropdown--light-background': lightBackground,
|
|
||||||
'server-sort-select-dropdown--mobile': isMobile,
|
|
||||||
}"
|
|
||||||
:style="dropdownStyle"
|
|
||||||
>
|
|
||||||
<div class="sort-select-options">
|
|
||||||
<div
|
|
||||||
v-for="item in options"
|
|
||||||
:key="item.value"
|
|
||||||
class="server-sort-item"
|
|
||||||
:class="{
|
|
||||||
active: activeValue === item.value,
|
|
||||||
}"
|
|
||||||
:title="item?.title || false"
|
|
||||||
@click.stop="handleSelect(item, $event)"
|
|
||||||
>
|
|
||||||
<span class="option-label">{{ item.label }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref } from 'vue';
|
|
||||||
|
|
||||||
defineProps({
|
|
||||||
visible: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
options: {
|
|
||||||
type: Array,
|
|
||||||
default: () => [],
|
|
||||||
},
|
|
||||||
activeValue: {
|
|
||||||
type: String,
|
|
||||||
default: '',
|
|
||||||
},
|
|
||||||
dropdownStyle: {
|
|
||||||
type: Object,
|
|
||||||
default: () => ({}),
|
|
||||||
},
|
|
||||||
lightBackground: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
isMobile: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const emits = defineEmits(['select']);
|
|
||||||
|
|
||||||
const dropdownRef = ref(null);
|
|
||||||
|
|
||||||
function handleSelect(item, event) {
|
|
||||||
event.stopPropagation();
|
|
||||||
emits('select', item);
|
|
||||||
}
|
|
||||||
|
|
||||||
defineExpose({
|
|
||||||
dropdownRef,
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.server-sort-select-dropdown {
|
|
||||||
z-index: 500;
|
|
||||||
background: rgba(#000, 0.8);
|
|
||||||
border-radius: 6px;
|
|
||||||
padding: 10px;
|
|
||||||
min-width: 150px;
|
|
||||||
max-height: 300px;
|
|
||||||
overflow-y: auto;
|
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
|
||||||
|
|
||||||
// 小屏居中显示样式
|
|
||||||
&--mobile {
|
|
||||||
min-width: 280px;
|
|
||||||
max-width: 90vw;
|
|
||||||
max-height: 70vh;
|
|
||||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.sort-select-options {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.server-sort-item {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
height: 36px;
|
|
||||||
padding: 0 15px;
|
|
||||||
line-height: 1.2;
|
|
||||||
border-radius: 6px;
|
|
||||||
background: rgba(#000, 0.3);
|
|
||||||
transition: all 0.3s linear;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
.option-label {
|
|
||||||
color: #fff;
|
|
||||||
font-weight: bold;
|
|
||||||
transition: all 0.3s linear;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
.option-label {
|
|
||||||
color: var(--option-high-color);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.active {
|
|
||||||
background: var(--option-high-color-active);
|
|
||||||
|
|
||||||
.option-label {
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 浅色背景样式
|
|
||||||
.server-sort-select-dropdown--light-background {
|
|
||||||
.server-sort-item {
|
|
||||||
background: rgba(#000, 0.5);
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: rgba(#000, 0.8);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.active {
|
|
||||||
background: var(--option-high-color-active);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -1,142 +0,0 @@
|
|||||||
<template>
|
|
||||||
<dot-dot-box
|
|
||||||
v-if="tableData"
|
|
||||||
border-radius="6px"
|
|
||||||
class="server-status"
|
|
||||||
>
|
|
||||||
<table class="server-status-table">
|
|
||||||
<thead class="server-status-table-header">
|
|
||||||
<tr class="server-status-table-header-row">
|
|
||||||
<template
|
|
||||||
v-for="column in tableData.columnProps"
|
|
||||||
:key="`th_${column.prop}`"
|
|
||||||
>
|
|
||||||
<template v-if="['billing', 'remainingTime'].includes(column.prop)">
|
|
||||||
<server-status-th
|
|
||||||
v-if="tableData.showBilling && column.prop === 'billing'"
|
|
||||||
:column="column"
|
|
||||||
/>
|
|
||||||
<server-status-th
|
|
||||||
v-if="tableData.showRemainingTime && column.prop === 'remainingTime'"
|
|
||||||
:column="column"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
<server-status-th
|
|
||||||
:column="column"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</template>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody class="server-status-table-body">
|
|
||||||
<tr
|
|
||||||
v-for="itemData in tableData.list"
|
|
||||||
:key="itemData.info.ID"
|
|
||||||
class="server-status-table-body-row"
|
|
||||||
:class="{
|
|
||||||
'server-status-table-body-row--offline': itemData.info?.online === -1,
|
|
||||||
'server-status-table-body-row--online': itemData.info?.online === 1,
|
|
||||||
[`server-item--${itemData.info?.ID}`]: true,
|
|
||||||
}"
|
|
||||||
@click="openDetail(itemData.info)"
|
|
||||||
>
|
|
||||||
<template
|
|
||||||
v-for="column in itemData.columnData"
|
|
||||||
:key="`td_${itemData.info?.ID}_${column.prop}`"
|
|
||||||
>
|
|
||||||
<template v-if="['billing', 'remainingTime'].includes(column.prop)">
|
|
||||||
<server-status-td
|
|
||||||
v-if="tableData.showBilling && column.prop === 'billing'"
|
|
||||||
:column="column"
|
|
||||||
/>
|
|
||||||
<server-status-td
|
|
||||||
v-if="tableData.showRemainingTime && column.prop === 'remainingTime'"
|
|
||||||
:column="column"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
<server-status-td
|
|
||||||
:column="column"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</template>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</dot-dot-box>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
/**
|
|
||||||
* ServerStatus风格的列表
|
|
||||||
*/
|
|
||||||
|
|
||||||
import {
|
|
||||||
computed,
|
|
||||||
} from 'vue';
|
|
||||||
import {
|
|
||||||
useRouter,
|
|
||||||
} from 'vue-router';
|
|
||||||
|
|
||||||
import config from '@/config';
|
|
||||||
|
|
||||||
import {
|
|
||||||
handleServerListColumn,
|
|
||||||
} from './server-status';
|
|
||||||
|
|
||||||
import ServerStatusTh from './table/th.vue';
|
|
||||||
import ServerStatusTd from './table/td.vue';
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
serverList: {
|
|
||||||
type: Array,
|
|
||||||
default: () => [],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const tableData = computed(() => {
|
|
||||||
const result = handleServerListColumn(props.serverList, config.nazhua.serverStatusColumnsTpl);
|
|
||||||
return result;
|
|
||||||
});
|
|
||||||
|
|
||||||
function openDetail(info) {
|
|
||||||
router.push({
|
|
||||||
name: 'ServerDetail',
|
|
||||||
params: {
|
|
||||||
serverId: info.ID,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.server-status {
|
|
||||||
--server-status-cell-padding: 0 5px;
|
|
||||||
--server-status-td-height: 32px;
|
|
||||||
|
|
||||||
--progress-bar-height: 18px;
|
|
||||||
}
|
|
||||||
.server-status-table {
|
|
||||||
width: 100%;
|
|
||||||
border-collapse: collapse;
|
|
||||||
|
|
||||||
.server-status-table-body-row {
|
|
||||||
@media screen and (min-width: 1025px) {
|
|
||||||
cursor: pointer;
|
|
||||||
background-color: rgba(255, 255, 255, 0);
|
|
||||||
transition: background-color 500ms ease-in-out;
|
|
||||||
&:hover {
|
|
||||||
background-color: rgba(255, 255, 255, 0.1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
&--offline td:not(.server-status-td--status) {
|
|
||||||
filter: grayscale(1);
|
|
||||||
opacity: 0.75;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -1,68 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="conn-group">
|
|
||||||
<div class="conn--tcp">
|
|
||||||
{{ tcpConnCount }}
|
|
||||||
</div>
|
|
||||||
<div class="split-line">
|
|
||||||
|
|
|
||||||
</div>
|
|
||||||
<div class="conn--udp">
|
|
||||||
{{ udpConnCount }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
/**
|
|
||||||
* 连接信息
|
|
||||||
*/
|
|
||||||
|
|
||||||
import {
|
|
||||||
computed,
|
|
||||||
} from 'vue';
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
realTimeData: {
|
|
||||||
type: Object,
|
|
||||||
default: () => ({}),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const tcpConnCount = computed(() => {
|
|
||||||
const { item } = props.realTimeData?.conns || {};
|
|
||||||
const { value } = item?.data?.tcp || {};
|
|
||||||
return value || '-';
|
|
||||||
});
|
|
||||||
const udpConnCount = computed(() => {
|
|
||||||
const { item } = props.realTimeData?.conns || {};
|
|
||||||
const { value } = item?.data?.udp || {};
|
|
||||||
return value || '-';
|
|
||||||
});
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.conn-group {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 5px;
|
|
||||||
width: 100%;
|
|
||||||
.conn--tcp {
|
|
||||||
flex: 1;
|
|
||||||
text-align: right;
|
|
||||||
color: var(--conn-tcp-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.conn--udp {
|
|
||||||
flex: 1;
|
|
||||||
text-align: left;
|
|
||||||
color: var(--conn-udp-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.split-line {
|
|
||||||
width: 6px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -1,35 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="country-content">
|
|
||||||
<server-flag :info="info" />
|
|
||||||
<span class="country-label">
|
|
||||||
{{ countryLabel }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
/**
|
|
||||||
* 地区信息
|
|
||||||
*/
|
|
||||||
|
|
||||||
import {
|
|
||||||
computed,
|
|
||||||
} from 'vue';
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
info: {
|
|
||||||
type: Object,
|
|
||||||
default: () => ({}),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const countryLabel = computed(() => props.info?.Host?.CountryCode?.toUpperCase() || 'UN');
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.country-content {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 5px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -1,74 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="net-speed-group">
|
|
||||||
<div class="net-speed--in">
|
|
||||||
{{ inSpeed }}
|
|
||||||
</div>
|
|
||||||
<div class="split-line">
|
|
||||||
|
|
|
||||||
</div>
|
|
||||||
<div class="net-speed--out">
|
|
||||||
{{ outSpeed }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
/**
|
|
||||||
* 网速信息
|
|
||||||
*/
|
|
||||||
|
|
||||||
import {
|
|
||||||
computed,
|
|
||||||
} from 'vue';
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
realTimeData: {
|
|
||||||
type: Object,
|
|
||||||
default: () => ({}),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const inSpeed = computed(() => {
|
|
||||||
const { item } = props.realTimeData?.speeds || {};
|
|
||||||
if (item?.data?.in) {
|
|
||||||
const { value, unit } = item.data.in;
|
|
||||||
return `${value}${unit}`;
|
|
||||||
}
|
|
||||||
return '-';
|
|
||||||
});
|
|
||||||
const outSpeed = computed(() => {
|
|
||||||
const { item } = props.realTimeData?.speeds || {};
|
|
||||||
if (item?.data?.out) {
|
|
||||||
const { value, unit } = item.data.out;
|
|
||||||
return `${value}${unit}`;
|
|
||||||
}
|
|
||||||
return '-';
|
|
||||||
});
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.net-speed-group {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 5px;
|
|
||||||
width: 100%;
|
|
||||||
.net-speed--in {
|
|
||||||
flex: 1;
|
|
||||||
text-align: right;
|
|
||||||
color: var(--net-speed-in-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.net-speed--out {
|
|
||||||
flex: 1;
|
|
||||||
text-align: left;
|
|
||||||
color: var(--net-speed-out-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.split-line {
|
|
||||||
width: 6px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -1,47 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="status-icon-box">
|
|
||||||
<div
|
|
||||||
class="status-icon"
|
|
||||||
:class="{
|
|
||||||
online: info.online === 1,
|
|
||||||
offline: info.online === -1,
|
|
||||||
}"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
/**
|
|
||||||
* 状态图标
|
|
||||||
*/
|
|
||||||
defineProps({
|
|
||||||
info: {
|
|
||||||
type: Object,
|
|
||||||
default: () => ({}),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.status-icon-box {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-icon {
|
|
||||||
width: 12px;
|
|
||||||
height: 12px;
|
|
||||||
border-radius: 50%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-icon.online {
|
|
||||||
background-image: linear-gradient(rgba(77, 133, 58, 1) 0, rgba(54, 126, 54, 1) 100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-icon.offline {
|
|
||||||
background-image: linear-gradient(rgba(155, 37, 34, 1) 0, rgba(161, 38, 35, 1) 100%);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -1,39 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="system-os-content">
|
|
||||||
<span class="system-icon">
|
|
||||||
<span :class="platformLogoIconClassName" />
|
|
||||||
</span>
|
|
||||||
<span class="system-label">
|
|
||||||
{{ systemOSLabel }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
/**
|
|
||||||
* 系统信息
|
|
||||||
*/
|
|
||||||
|
|
||||||
import {
|
|
||||||
computed,
|
|
||||||
} from 'vue';
|
|
||||||
import * as hostUtils from '@/utils/host';
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
info: {
|
|
||||||
type: Object,
|
|
||||||
default: () => ({}),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const platformLogoIconClassName = computed(() => hostUtils.getPlatformLogoIconClassName(props.info?.Host?.Platform));
|
|
||||||
const systemOSLabel = computed(() => hostUtils.getSystemOSLabel(props.info?.Host?.Platform, true));
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.system-os-content {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 5px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -1,74 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="transfer-group">
|
|
||||||
<div class="transfer--in">
|
|
||||||
{{ transferIn }}
|
|
||||||
</div>
|
|
||||||
<div class="split-line">
|
|
||||||
|
|
|
||||||
</div>
|
|
||||||
<div class="transfer--out">
|
|
||||||
{{ transferOut }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
/**
|
|
||||||
* 流量信息
|
|
||||||
*/
|
|
||||||
|
|
||||||
import {
|
|
||||||
computed,
|
|
||||||
} from 'vue';
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
realTimeData: {
|
|
||||||
type: Object,
|
|
||||||
default: () => ({}),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const transferIn = computed(() => {
|
|
||||||
const { item } = props.realTimeData?.transfer || {};
|
|
||||||
if (item?.data?.in) {
|
|
||||||
const { value, unit } = item.data.in;
|
|
||||||
return `${value}${unit}`;
|
|
||||||
}
|
|
||||||
return '-';
|
|
||||||
});
|
|
||||||
const transferOut = computed(() => {
|
|
||||||
const { item } = props.realTimeData?.transfer || {};
|
|
||||||
if (item?.data?.out) {
|
|
||||||
const { value, unit } = item.data.out;
|
|
||||||
return `${value}${unit}`;
|
|
||||||
}
|
|
||||||
return '-';
|
|
||||||
});
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.transfer-group {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 5px;
|
|
||||||
width: 100%;
|
|
||||||
.transfer--in {
|
|
||||||
flex: 1;
|
|
||||||
text-align: right;
|
|
||||||
color: var(--transfer-in-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.transfer--out {
|
|
||||||
flex: 1;
|
|
||||||
text-align: left;
|
|
||||||
color: var(--transfer-out-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.split-line {
|
|
||||||
width: 6px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -1,446 +0,0 @@
|
|||||||
/**
|
|
||||||
* ServerStatus风格的列表列配置
|
|
||||||
*/
|
|
||||||
import {
|
|
||||||
h,
|
|
||||||
} from 'vue';
|
|
||||||
|
|
||||||
// import * as hostUtils from '@/utils/host';
|
|
||||||
import handleServerStatus from '@/views/composable/server-status';
|
|
||||||
import handleServerInfo from '@/views/composable/server-info';
|
|
||||||
import handleServerRealTime from '@/views/composable/server-real-time';
|
|
||||||
import handleServerBillAndPlan from '@/views/composable/server-bill-and-plan';
|
|
||||||
|
|
||||||
import ServerStatusProgress from '@/views/components/server/server-status-progress.vue';
|
|
||||||
import StatusIcon from '@/views/components/server-list/server-status/server-info/status-icon.vue';
|
|
||||||
import SystemOS from '@/views/components/server-list/server-status/server-info/system-os.vue';
|
|
||||||
import Country from '@/views/components/server-list/server-status/server-info/country.vue';
|
|
||||||
import NetSpeed from '@/views/components/server-list/server-status/server-info/net-speed.vue';
|
|
||||||
import Transfer from '@/views/components/server-list/server-status/server-info/transfer.vue';
|
|
||||||
import Conns from '@/views/components/server-list/server-status/server-info/conns.vue';
|
|
||||||
|
|
||||||
const COLUMN_MAP = Object.freeze({
|
|
||||||
status: {
|
|
||||||
label: '状态',
|
|
||||||
width: 40,
|
|
||||||
},
|
|
||||||
name: {
|
|
||||||
label: '名称',
|
|
||||||
minWidth: 100,
|
|
||||||
align: 'left',
|
|
||||||
},
|
|
||||||
config: {
|
|
||||||
label: '规格',
|
|
||||||
width: 80,
|
|
||||||
align: 'left',
|
|
||||||
},
|
|
||||||
system: {
|
|
||||||
label: '系统',
|
|
||||||
width: 90,
|
|
||||||
align: 'left',
|
|
||||||
},
|
|
||||||
country: {
|
|
||||||
label: '地区',
|
|
||||||
width: 60,
|
|
||||||
align: 'left',
|
|
||||||
},
|
|
||||||
duration: {
|
|
||||||
label: '在线',
|
|
||||||
width: 60,
|
|
||||||
align: 'left',
|
|
||||||
},
|
|
||||||
load: {
|
|
||||||
label: '负载',
|
|
||||||
width: 45,
|
|
||||||
align: 'center',
|
|
||||||
},
|
|
||||||
speeds: {
|
|
||||||
label: '网速',
|
|
||||||
width: 122,
|
|
||||||
align: 'center',
|
|
||||||
},
|
|
||||||
inSpeed: {
|
|
||||||
label: '入网',
|
|
||||||
width: 60,
|
|
||||||
align: 'left',
|
|
||||||
},
|
|
||||||
outSpeed: {
|
|
||||||
label: '出网',
|
|
||||||
width: 60,
|
|
||||||
align: 'left',
|
|
||||||
},
|
|
||||||
transfer: {
|
|
||||||
label: '流量',
|
|
||||||
width: 122,
|
|
||||||
align: 'center',
|
|
||||||
},
|
|
||||||
inTransfer: {
|
|
||||||
label: '入网流量',
|
|
||||||
width: 60,
|
|
||||||
align: 'left',
|
|
||||||
},
|
|
||||||
outTransfer: {
|
|
||||||
label: '出网流量',
|
|
||||||
width: 60,
|
|
||||||
align: 'left',
|
|
||||||
},
|
|
||||||
conns: {
|
|
||||||
label: '连接',
|
|
||||||
width: 72,
|
|
||||||
align: 'center',
|
|
||||||
},
|
|
||||||
tcp: {
|
|
||||||
label: 'TCP',
|
|
||||||
width: 40,
|
|
||||||
align: 'left',
|
|
||||||
},
|
|
||||||
udp: {
|
|
||||||
label: 'UDP',
|
|
||||||
width: 40,
|
|
||||||
align: 'left',
|
|
||||||
},
|
|
||||||
cpu: {
|
|
||||||
label: 'CPU',
|
|
||||||
width: 80,
|
|
||||||
align: 'center',
|
|
||||||
},
|
|
||||||
cpuText: {
|
|
||||||
valProp: 'cpu',
|
|
||||||
label: 'CPU',
|
|
||||||
width: 40,
|
|
||||||
align: 'center',
|
|
||||||
},
|
|
||||||
mem: {
|
|
||||||
label: '内存',
|
|
||||||
width: 80,
|
|
||||||
align: 'center',
|
|
||||||
},
|
|
||||||
memText: {
|
|
||||||
valProp: 'mem',
|
|
||||||
label: '内存',
|
|
||||||
width: 40,
|
|
||||||
align: 'center',
|
|
||||||
},
|
|
||||||
swap: {
|
|
||||||
label: '交换',
|
|
||||||
width: 80,
|
|
||||||
align: 'center',
|
|
||||||
},
|
|
||||||
swapText: {
|
|
||||||
valProp: 'swap',
|
|
||||||
label: '交换',
|
|
||||||
width: 40,
|
|
||||||
align: 'center',
|
|
||||||
},
|
|
||||||
disk: {
|
|
||||||
label: '硬盘',
|
|
||||||
width: 80,
|
|
||||||
align: 'center',
|
|
||||||
},
|
|
||||||
diskText: {
|
|
||||||
valProp: 'disk',
|
|
||||||
label: '硬盘',
|
|
||||||
width: 40,
|
|
||||||
align: 'center',
|
|
||||||
},
|
|
||||||
billing: {
|
|
||||||
label: '价格',
|
|
||||||
width: 100,
|
|
||||||
align: 'right',
|
|
||||||
},
|
|
||||||
remainingTime: {
|
|
||||||
label: '剩余',
|
|
||||||
width: 70,
|
|
||||||
align: 'right',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 默认列配置
|
|
||||||
*/
|
|
||||||
// eslint-disable-next-line max-len, vue/max-len
|
|
||||||
const DEFAULT_COLUMNS = 'status,name,country,system,config,duration,speeds,transfer,load,cpu,mem,disk,billing,remainingTime';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 需要实时更新的数据
|
|
||||||
*/
|
|
||||||
const RELD_TIME_DATA = [
|
|
||||||
'speeds', 'inSpeed', 'outSpeed',
|
|
||||||
'transfer', 'inTransfer', 'outTransfer',
|
|
||||||
'conns', 'tcp', 'udp',
|
|
||||||
'duration', 'load',
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取列配置
|
|
||||||
* @param {string} columnsTpls 列配置模板
|
|
||||||
* @returns {Object} 列配置
|
|
||||||
* @property {Array} columns 列配置
|
|
||||||
*/
|
|
||||||
export const getColumnPropsConfig = (tpls = DEFAULT_COLUMNS) => {
|
|
||||||
const tplList = tpls.split(',');
|
|
||||||
const columnList = [];
|
|
||||||
tplList.forEach((tpl) => {
|
|
||||||
if (COLUMN_MAP[tpl]) {
|
|
||||||
columnList.push({
|
|
||||||
prop: tpl,
|
|
||||||
...COLUMN_MAP[tpl],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return columnList;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 将服务器数据转换为表格数据
|
|
||||||
* @param {Object} server 服务器数据
|
|
||||||
* @returns {Object} 表格数据
|
|
||||||
*/
|
|
||||||
export const handleServerItemData = (params) => {
|
|
||||||
const {
|
|
||||||
column,
|
|
||||||
server,
|
|
||||||
realTimeData,
|
|
||||||
progressData,
|
|
||||||
billAndPlan,
|
|
||||||
} = params || {};
|
|
||||||
switch (column.prop) {
|
|
||||||
case 'status':
|
|
||||||
return {
|
|
||||||
type: 'component',
|
|
||||||
component: h(StatusIcon, { info: server }),
|
|
||||||
originalData: params,
|
|
||||||
};
|
|
||||||
case 'name':
|
|
||||||
return {
|
|
||||||
type: 'text',
|
|
||||||
value: server.Name,
|
|
||||||
originalData: params,
|
|
||||||
};
|
|
||||||
case 'config':
|
|
||||||
{
|
|
||||||
const { cpuAndMemAndDisk } = handleServerInfo({
|
|
||||||
props: {
|
|
||||||
info: server,
|
|
||||||
},
|
|
||||||
originalData: params,
|
|
||||||
});
|
|
||||||
return {
|
|
||||||
type: 'text',
|
|
||||||
value: cpuAndMemAndDisk,
|
|
||||||
originalData: params,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
case 'system':
|
|
||||||
return {
|
|
||||||
type: 'component',
|
|
||||||
component: h(SystemOS, { info: server }),
|
|
||||||
originalData: params,
|
|
||||||
};
|
|
||||||
case 'country':
|
|
||||||
return {
|
|
||||||
type: 'component',
|
|
||||||
component: h(Country, { info: server }),
|
|
||||||
originalData: params,
|
|
||||||
};
|
|
||||||
case 'speeds':
|
|
||||||
return {
|
|
||||||
type: 'component',
|
|
||||||
component: h(NetSpeed, { realTimeData }),
|
|
||||||
originalData: params,
|
|
||||||
};
|
|
||||||
case 'transfer':
|
|
||||||
return {
|
|
||||||
type: 'component',
|
|
||||||
component: h(Transfer, { realTimeData }),
|
|
||||||
originalData: params,
|
|
||||||
};
|
|
||||||
case 'conns':
|
|
||||||
return {
|
|
||||||
type: 'component',
|
|
||||||
component: h(Conns, { realTimeData }),
|
|
||||||
originalData: params,
|
|
||||||
};
|
|
||||||
case 'cpu':
|
|
||||||
case 'mem':
|
|
||||||
case 'disk':
|
|
||||||
case 'swap':
|
|
||||||
{
|
|
||||||
const progressItem = progressData[column.prop];
|
|
||||||
return {
|
|
||||||
type: 'component',
|
|
||||||
component: h(ServerStatusProgress, {
|
|
||||||
type: column.prop,
|
|
||||||
used: progressItem?.used || 0,
|
|
||||||
colors: progressItem?.colors || {},
|
|
||||||
valText: progressItem?.valPercent || '',
|
|
||||||
}),
|
|
||||||
originalData: params,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
case 'cpuText':
|
|
||||||
case 'memText':
|
|
||||||
case 'diskText':
|
|
||||||
case 'swapText':
|
|
||||||
{
|
|
||||||
const progressItem = progressData[column.valProp];
|
|
||||||
return {
|
|
||||||
prop: column.prop,
|
|
||||||
type: 'text',
|
|
||||||
value: parseFloat(progressItem?.used || 0).toFixed(1),
|
|
||||||
unit: '%',
|
|
||||||
text: progressItem?.valPercent || '',
|
|
||||||
originalData: params,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
case 'billing':
|
|
||||||
{
|
|
||||||
const item = billAndPlan?.value?.billing;
|
|
||||||
const texts = [];
|
|
||||||
if (item?.value) {
|
|
||||||
texts.push(item.value || '-');
|
|
||||||
}
|
|
||||||
if (item?.cycleLabel) {
|
|
||||||
texts.push(item.cycleLabel);
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
prop: column.prop,
|
|
||||||
type: 'text',
|
|
||||||
text: texts.length ? texts.join('/') : '-',
|
|
||||||
originalData: params,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
case 'remainingTime':
|
|
||||||
{
|
|
||||||
const item = billAndPlan?.value?.remainingTime;
|
|
||||||
return {
|
|
||||||
prop: column.prop,
|
|
||||||
type: 'text',
|
|
||||||
text: item?.value || '-',
|
|
||||||
originalData: params,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
if (RELD_TIME_DATA.includes(column.prop) && realTimeData[column.prop]) {
|
|
||||||
const item = realTimeData[column.prop];
|
|
||||||
return {
|
|
||||||
prop: column.prop,
|
|
||||||
type: 'text',
|
|
||||||
text: item?.text,
|
|
||||||
value: item?.value,
|
|
||||||
unit: item?.unit,
|
|
||||||
originalData: params,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
prop: column.prop,
|
|
||||||
type: 'text',
|
|
||||||
value: '-',
|
|
||||||
originalData: params,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 将服务器数据转换为表格数据
|
|
||||||
* @param {Object} server 服务器数据
|
|
||||||
* @param {Array} columns 列配置
|
|
||||||
* @returns {Array} 表格数据
|
|
||||||
*/
|
|
||||||
export const handleServerListColumn = (serverList, columnTpls = DEFAULT_COLUMNS) => {
|
|
||||||
const columnProps = getColumnPropsConfig(columnTpls);
|
|
||||||
const tpls = columnProps.map((column) => column.valProp || column.prop).join(',');
|
|
||||||
const hasBilling = columnTpls.includes('billing');
|
|
||||||
const hasRemainingTime = columnTpls.includes('remainingTime');
|
|
||||||
let showBilling = false;
|
|
||||||
let showRemainingTime = false;
|
|
||||||
const list = serverList.map((server) => {
|
|
||||||
// 负载\网速\流量\在线等
|
|
||||||
const realTimeResult = handleServerRealTime({
|
|
||||||
props: {
|
|
||||||
info: server,
|
|
||||||
},
|
|
||||||
serverRealTimeListTpls: tpls,
|
|
||||||
});
|
|
||||||
const realTimeData = {};
|
|
||||||
realTimeResult?.serverRealTimeList?.value?.forEach?.((item) => {
|
|
||||||
if (item.show) {
|
|
||||||
const text = [item.value];
|
|
||||||
if (item.unit) {
|
|
||||||
text.push(item.unit);
|
|
||||||
}
|
|
||||||
realTimeData[item.key] = {
|
|
||||||
value: item.value,
|
|
||||||
unit: item.unit,
|
|
||||||
text: text.join(''),
|
|
||||||
item,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
realTimeData[item.key] = {
|
|
||||||
text: '-',
|
|
||||||
item,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// CPU\内存\硬盘\交换 进度条
|
|
||||||
const {
|
|
||||||
serverStatusList,
|
|
||||||
} = handleServerStatus({
|
|
||||||
props: {
|
|
||||||
info: server,
|
|
||||||
},
|
|
||||||
statusListTpl: tpls,
|
|
||||||
statusListItemContent: false,
|
|
||||||
});
|
|
||||||
const progressData = {};
|
|
||||||
serverStatusList.value?.forEach?.((item) => {
|
|
||||||
progressData[item.type] = item;
|
|
||||||
});
|
|
||||||
let billAndPlan = null;
|
|
||||||
if (hasBilling || hasRemainingTime) {
|
|
||||||
const result = handleServerBillAndPlan({
|
|
||||||
props: {
|
|
||||||
info: server,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
billAndPlan = result.billAndPlan;
|
|
||||||
if (billAndPlan?.value?.billing) {
|
|
||||||
showBilling = true;
|
|
||||||
}
|
|
||||||
if (billAndPlan?.value?.remainingTime) {
|
|
||||||
showRemainingTime = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const columnData = [];
|
|
||||||
columnProps.forEach((columnItem) => {
|
|
||||||
columnData.push({
|
|
||||||
...columnItem,
|
|
||||||
data: handleServerItemData({
|
|
||||||
column: columnItem,
|
|
||||||
server,
|
|
||||||
realTimeData,
|
|
||||||
progressData,
|
|
||||||
billAndPlan,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
info: server,
|
|
||||||
columnData,
|
|
||||||
computedData: {
|
|
||||||
realTimeData,
|
|
||||||
progressData,
|
|
||||||
billAndPlan,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
return {
|
|
||||||
list,
|
|
||||||
columnProps,
|
|
||||||
showBilling,
|
|
||||||
showRemainingTime,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@ -1,209 +0,0 @@
|
|||||||
<template>
|
|
||||||
<td
|
|
||||||
class="server-status-td server-status-body-td"
|
|
||||||
:class="columnClass"
|
|
||||||
:style="columnStyle"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="server-status-td-content"
|
|
||||||
:class="'server-status-td-content--' + tdContent.prop"
|
|
||||||
>
|
|
||||||
<template
|
|
||||||
v-if="tdContent.type === 'text'"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
v-if="isSet(tdContent.value)"
|
|
||||||
class="text--value"
|
|
||||||
>
|
|
||||||
{{ tdContent.value }}
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
v-if="isSet(tdContent.unit)"
|
|
||||||
class="text--unit"
|
|
||||||
>
|
|
||||||
{{ tdContent.unit }}
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
v-if="!isSet(tdContent.value) && isSet(tdContent.text)"
|
|
||||||
class="text"
|
|
||||||
>
|
|
||||||
{{ tdContent.text }}
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
<template
|
|
||||||
v-if="tdContent.type === 'component'"
|
|
||||||
>
|
|
||||||
<component :is="tdContent.component" />
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
/**
|
|
||||||
* 自定义TD组件
|
|
||||||
*/
|
|
||||||
|
|
||||||
import {
|
|
||||||
computed,
|
|
||||||
} from 'vue';
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
column: {
|
|
||||||
type: Object,
|
|
||||||
default: () => ({}),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// 计算css的长度单位
|
|
||||||
const getCssLengthUnit = (value) => {
|
|
||||||
if (typeof value === 'number') {
|
|
||||||
return `${value}px`;
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
};
|
|
||||||
|
|
||||||
const columnClass = computed(() => {
|
|
||||||
const className = {
|
|
||||||
[`server-status-td--${props.column.prop}`]: true,
|
|
||||||
};
|
|
||||||
if (props.column.align) {
|
|
||||||
className[`server-status-td--align-${props.column.align}`] = true;
|
|
||||||
}
|
|
||||||
return className;
|
|
||||||
});
|
|
||||||
|
|
||||||
const columnStyle = computed(() => {
|
|
||||||
const style = {};
|
|
||||||
if (props.column.width) {
|
|
||||||
style.width = getCssLengthUnit(props.column.width);
|
|
||||||
}
|
|
||||||
if (props.column.minWidth) {
|
|
||||||
style.minWidth = getCssLengthUnit(props.column.minWidth);
|
|
||||||
}
|
|
||||||
return style;
|
|
||||||
});
|
|
||||||
|
|
||||||
const tdContent = computed(() => {
|
|
||||||
if (['text', 'component'].includes(props.column.data.type)) {
|
|
||||||
return props.column.data;
|
|
||||||
}
|
|
||||||
return '';
|
|
||||||
});
|
|
||||||
|
|
||||||
function isSet(value) {
|
|
||||||
return value !== undefined && value !== null && value !== '';
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.server-status-td {
|
|
||||||
height: var(--server-status-td-height);
|
|
||||||
padding: var(--server-status-cell-padding);
|
|
||||||
|
|
||||||
--td-content-justify-content: center;
|
|
||||||
|
|
||||||
&--align-center {
|
|
||||||
--td-content-justify-content: center;
|
|
||||||
}
|
|
||||||
&--align-right {
|
|
||||||
--td-content-justify-content: flex-end;
|
|
||||||
}
|
|
||||||
&--align-left {
|
|
||||||
--td-content-justify-content: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.server-status-td-content {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: var(--td-content-justify-content);
|
|
||||||
width: 100%;
|
|
||||||
line-height: var(--server-status-td-height);
|
|
||||||
|
|
||||||
&--transfer {
|
|
||||||
.text--value {
|
|
||||||
color: var(--transfer-color);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&--inTransfer {
|
|
||||||
.text--value {
|
|
||||||
color: var(--transfer-in-color);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
&--outTransfer {
|
|
||||||
.text--value {
|
|
||||||
color: var(--transfer-out-color);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&--inSpeed {
|
|
||||||
.text--value {
|
|
||||||
color: var(--net-speed-in-color);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&--outSpeed {
|
|
||||||
.text--value {
|
|
||||||
color: var(--net-speed-out-color);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&--tcp {
|
|
||||||
.text--value {
|
|
||||||
color: var(--conn-tcp-color);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&--udp {
|
|
||||||
.text--value {
|
|
||||||
color: var(--conn-udp-color);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&--load {
|
|
||||||
.text--value {
|
|
||||||
color: var(--load-color);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&--duration {
|
|
||||||
.text--value {
|
|
||||||
color: var(--duration-color);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&--cpuText {
|
|
||||||
.text--value {
|
|
||||||
color: var(--cpu-text-color);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&--memText {
|
|
||||||
.text--value {
|
|
||||||
color: var(--mem-text-color);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&--swapText {
|
|
||||||
.text--value {
|
|
||||||
color: var(--swap-text-color);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&--diskText {
|
|
||||||
.text--value {
|
|
||||||
color: var(--disk-text-color);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&--billing {
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&--remainingTime {
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -1,72 +0,0 @@
|
|||||||
<template>
|
|
||||||
<th
|
|
||||||
class="server-status-th"
|
|
||||||
:class="columnClass"
|
|
||||||
:style="columnStyle"
|
|
||||||
>
|
|
||||||
{{ column.label }}
|
|
||||||
</th>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
/**
|
|
||||||
* 自定义TH组件
|
|
||||||
*/
|
|
||||||
|
|
||||||
import {
|
|
||||||
computed,
|
|
||||||
} from 'vue';
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
column: {
|
|
||||||
type: Object,
|
|
||||||
default: () => ({}),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// 计算css的长度单位
|
|
||||||
const getCssLengthUnit = (value) => {
|
|
||||||
if (typeof value === 'number') {
|
|
||||||
return `${value}px`;
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
};
|
|
||||||
|
|
||||||
const columnClass = computed(() => {
|
|
||||||
const className = {};
|
|
||||||
if (props.column.align) {
|
|
||||||
className[`server-status-th--align-${props.column.align}`] = true;
|
|
||||||
}
|
|
||||||
return className;
|
|
||||||
});
|
|
||||||
|
|
||||||
const columnStyle = computed(() => {
|
|
||||||
const style = {};
|
|
||||||
if (props.column.width) {
|
|
||||||
style.width = getCssLengthUnit(props.column.width);
|
|
||||||
}
|
|
||||||
if (props.column.minWidth) {
|
|
||||||
style.minWidth = getCssLengthUnit(props.column.minWidth);
|
|
||||||
}
|
|
||||||
return style;
|
|
||||||
});
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.server-status-th {
|
|
||||||
padding: var(--server-status-cell-padding);
|
|
||||||
|
|
||||||
text-align: center;
|
|
||||||
|
|
||||||
&--align-center {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
&--align-right {
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
&--align-left {
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -134,13 +134,11 @@ const {
|
|||||||
.item-content-sub-label {
|
.item-content-sub-label {
|
||||||
height: var(--real-time-label-line-height);
|
height: var(--real-time-label-line-height);
|
||||||
line-height: var(--real-time-label-line-height);
|
line-height: var(--real-time-label-line-height);
|
||||||
white-space: nowrap;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.item-content-sub-content {
|
.item-content-sub-content {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
white-space: nowrap;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.item-value,
|
.item-value,
|
||||||
|
|||||||
@ -12,10 +12,7 @@
|
|||||||
class="progress-bar-label"
|
class="progress-bar-label"
|
||||||
:title="label + '使用' + used + '%'"
|
:title="label + '使用' + used + '%'"
|
||||||
>
|
>
|
||||||
<span
|
<span class="server-status-label">
|
||||||
v-if="label"
|
|
||||||
class="server-status-label"
|
|
||||||
>
|
|
||||||
{{ label }}:
|
{{ label }}:
|
||||||
</span>
|
</span>
|
||||||
<span class="server-status-val-text">
|
<span class="server-status-val-text">
|
||||||
@ -99,13 +96,13 @@ const progressStyle = computed(() => {
|
|||||||
|
|
||||||
@media screen and (max-width: 480px) {
|
@media screen and (max-width: 480px) {
|
||||||
flex: none;
|
flex: none;
|
||||||
width: var(--progress-bar-width, calc(50% - 5px));
|
width: calc(50% - 5px);
|
||||||
}
|
}
|
||||||
|
|
||||||
// @media screen and (max-width: 350px) {
|
@media screen and (max-width: 350px) {
|
||||||
// flex: none;
|
flex: none;
|
||||||
// width: 100%;
|
width: 100%;
|
||||||
// }
|
}
|
||||||
|
|
||||||
.progress-bar-box {
|
.progress-bar-box {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|||||||
@ -1,75 +1,33 @@
|
|||||||
import uniqolor from 'uniqolor';
|
import uniqolor from 'uniqolor';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 计算数据的统计信息,使用截尾中位数作为基准值
|
* 计算数据的阈值和平均值
|
||||||
* 根据平均延迟的不同范围,使用不同的容差百分比进行削峰
|
|
||||||
*
|
*
|
||||||
* @param {number[]} data - 要计算的数据数组
|
* @param {number[]} data - 要计算的数据数组
|
||||||
* @returns {{median: number, tolerancePercent: number, min: number, max: number}}
|
* @param {number} [tolerance=2] - 容差倍数,默认值为2
|
||||||
* 返回包含统计信息的对象
|
* @returns {{threshold: number, mean: number}} 返回包含阈值和平均值的对象
|
||||||
* @property {number} median - 截尾中位数(去掉极端值后的中位数)
|
* @property {number} threshold - 计算得到的阈值
|
||||||
* @property {number} tolerancePercent - 根据中位数计算的容差百分比
|
* @property {number} mean - 数据的平均值
|
||||||
* @property {number} min - 最小值
|
|
||||||
* @property {number} max - 最大值
|
|
||||||
*/
|
*/
|
||||||
export function getThreshold(data) {
|
export function getThreshold(data, tolerance = 2) {
|
||||||
// 过滤掉null和0的数据,只对有效延迟值计算统计量
|
// 计算数据的平均值
|
||||||
|
const mean = data.reduce((sum, value) => sum + (value || 0), 0) / data.length;
|
||||||
|
// 计算数据的方差
|
||||||
|
const variance = data.reduce((sum, value) => sum + ((value || 0) - mean) ** 2, 0) / data.length;
|
||||||
|
// 计算标准差
|
||||||
|
const stdDev = Math.sqrt(variance);
|
||||||
|
// 计算阈值
|
||||||
|
const threshold = tolerance * stdDev;
|
||||||
|
// 过滤掉值为0的数据
|
||||||
const filteredData = data.filter((value) => value !== 0 && value !== null);
|
const filteredData = data.filter((value) => value !== 0 && value !== null);
|
||||||
|
// 计算过滤后数据的最小值
|
||||||
if (filteredData.length === 0) {
|
const min = Math.min(...filteredData);
|
||||||
return {
|
// 计算过滤后数据的最大值
|
||||||
median: 0,
|
const max = Math.max(...filteredData);
|
||||||
tolerancePercent: 0.2,
|
// 返回包含阈值、平均值、最小值和最大值的对象
|
||||||
min: 0,
|
|
||||||
max: 0,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// 排序数据
|
|
||||||
const sortedData = [...filteredData].sort((a, b) => Math.ceil(a) - Math.ceil(b));
|
|
||||||
const len = sortedData.length;
|
|
||||||
|
|
||||||
// 计算需要裁剪的数量(10%)
|
|
||||||
const trimCount = Math.floor(len * 0.1);
|
|
||||||
|
|
||||||
// 用于计算中位数的数据:如果10%的数量>=1,则去掉最大和最小的10%
|
|
||||||
let dataForMedian;
|
|
||||||
if (trimCount >= 1) {
|
|
||||||
// 截尾:去掉最小的10%和最大的10%
|
|
||||||
dataForMedian = sortedData.slice(trimCount, len - trimCount);
|
|
||||||
} else {
|
|
||||||
// 数据量太少,不裁剪
|
|
||||||
dataForMedian = sortedData;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 计算截尾中位数
|
|
||||||
const medianLen = dataForMedian.length;
|
|
||||||
const median = medianLen % 2 === 0
|
|
||||||
? (dataForMedian[medianLen / 2 - 1] + dataForMedian[medianLen / 2]) / 2
|
|
||||||
: dataForMedian[Math.floor(medianLen / 2)];
|
|
||||||
|
|
||||||
// 根据中位数确定容差百分比,延迟越小容差越大
|
|
||||||
let tolerancePercent;
|
|
||||||
if (median <= 10) {
|
|
||||||
tolerancePercent = 0.50; // 50%
|
|
||||||
} else if (median <= 30) {
|
|
||||||
tolerancePercent = 0.35; // 35%
|
|
||||||
} else if (median <= 50) {
|
|
||||||
tolerancePercent = 0.25; // 25%
|
|
||||||
} else if (median <= 100) {
|
|
||||||
tolerancePercent = 0.20; // 20%
|
|
||||||
} else {
|
|
||||||
tolerancePercent = 0.15; // 15%
|
|
||||||
}
|
|
||||||
|
|
||||||
const min = sortedData[0];
|
|
||||||
const max = sortedData[len - 1];
|
|
||||||
|
|
||||||
// console.log(min, max, median, sortedData);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
median,
|
threshold,
|
||||||
tolerancePercent,
|
mean,
|
||||||
min,
|
min,
|
||||||
max,
|
max,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -128,13 +128,7 @@ export default (params) => {
|
|||||||
value: 0,
|
value: 0,
|
||||||
unit: '',
|
unit: '',
|
||||||
};
|
};
|
||||||
if (inStats.p > 1) {
|
if (inStats.g > 1) {
|
||||||
result.value = (inStats.p).toFixed(1) * 1;
|
|
||||||
result.unit = 'P';
|
|
||||||
} else if (inStats.t > 1) {
|
|
||||||
result.value = (inStats.t).toFixed(1) * 1;
|
|
||||||
result.unit = 'T';
|
|
||||||
} else if (inStats.g > 1) {
|
|
||||||
result.value = (inStats.g).toFixed(1) * 1;
|
result.value = (inStats.g).toFixed(1) * 1;
|
||||||
result.unit = 'G';
|
result.unit = 'G';
|
||||||
} else if (inStats.m > 1) {
|
} else if (inStats.m > 1) {
|
||||||
@ -153,13 +147,7 @@ export default (params) => {
|
|||||||
value: 0,
|
value: 0,
|
||||||
unit: '',
|
unit: '',
|
||||||
};
|
};
|
||||||
if (outStats.p > 1) {
|
if (outStats.g > 1) {
|
||||||
result.value = (outStats.p).toFixed(1) * 1;
|
|
||||||
result.unit = 'P';
|
|
||||||
} else if (outStats.t > 1) {
|
|
||||||
result.value = (outStats.t).toFixed(1) * 1;
|
|
||||||
result.unit = 'T';
|
|
||||||
} else if (outStats.g > 1) {
|
|
||||||
result.value = (outStats.g).toFixed(1) * 1;
|
result.value = (outStats.g).toFixed(1) * 1;
|
||||||
result.unit = 'G';
|
result.unit = 'G';
|
||||||
} else if (outStats.m > 1) {
|
} else if (outStats.m > 1) {
|
||||||
@ -233,18 +221,6 @@ export default (params) => {
|
|||||||
value: transfer.value?.value,
|
value: transfer.value?.value,
|
||||||
unit: transfer.value?.unit,
|
unit: transfer.value?.unit,
|
||||||
show: validate.isSet(transfer.value?.value),
|
show: validate.isSet(transfer.value?.value),
|
||||||
data: {
|
|
||||||
in: {
|
|
||||||
value: inTransfer.value?.value,
|
|
||||||
unit: inTransfer.value?.unit,
|
|
||||||
show: validate.isSet(inTransfer.value?.value),
|
|
||||||
},
|
|
||||||
out: {
|
|
||||||
value: outTransfer.value?.value,
|
|
||||||
unit: outTransfer.value?.unit,
|
|
||||||
show: validate.isSet(outTransfer.value?.value),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
case 'inTransfer':
|
case 'inTransfer':
|
||||||
return {
|
return {
|
||||||
@ -287,18 +263,6 @@ export default (params) => {
|
|||||||
`${netOutSpeed.value?.value}${netOutSpeed.value?.unit}`,
|
`${netOutSpeed.value?.value}${netOutSpeed.value?.unit}`,
|
||||||
].join('|'),
|
].join('|'),
|
||||||
show: validate.isSet(netInSpeed.value?.value) && validate.isSet(netOutSpeed.value?.value),
|
show: validate.isSet(netInSpeed.value?.value) && validate.isSet(netOutSpeed.value?.value),
|
||||||
data: {
|
|
||||||
in: {
|
|
||||||
value: netInSpeed.value?.value,
|
|
||||||
unit: netInSpeed.value?.unit,
|
|
||||||
show: validate.isSet(netInSpeed.value?.value),
|
|
||||||
},
|
|
||||||
out: {
|
|
||||||
value: netOutSpeed.value?.value,
|
|
||||||
unit: netOutSpeed.value?.unit,
|
|
||||||
show: validate.isSet(netOutSpeed.value?.value),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
case 'load':
|
case 'load':
|
||||||
return {
|
return {
|
||||||
@ -307,49 +271,12 @@ export default (params) => {
|
|||||||
value: (props.info.State?.Load1 || 0).toFixed(2),
|
value: (props.info.State?.Load1 || 0).toFixed(2),
|
||||||
show: validate.isSet(props.info.State?.Load1),
|
show: validate.isSet(props.info.State?.Load1),
|
||||||
};
|
};
|
||||||
case 'loads':
|
|
||||||
{
|
|
||||||
const loads = [];
|
|
||||||
loads.push((props.info.State?.Load1 || 0).toFixed(2));
|
|
||||||
loads.push((props.info.State?.Load5 || 0).toFixed(2));
|
|
||||||
loads.push((props.info.State?.Load15 || 0).toFixed(2));
|
|
||||||
return {
|
|
||||||
key,
|
|
||||||
label: '负载',
|
|
||||||
value: loads.join(','),
|
|
||||||
show: loads.some((load) => validate.isSet(load)),
|
|
||||||
data: {
|
|
||||||
load1: {
|
|
||||||
value: (props.info.State?.Load1 || 0).toFixed(2),
|
|
||||||
show: validate.isSet(props.info.State?.Load1),
|
|
||||||
},
|
|
||||||
load5: {
|
|
||||||
value: (props.info.State?.Load5 || 0).toFixed(2),
|
|
||||||
show: validate.isSet(props.info.State?.Load5),
|
|
||||||
},
|
|
||||||
load15: {
|
|
||||||
value: (props.info.State?.Load15 || 0).toFixed(2),
|
|
||||||
show: validate.isSet(props.info.State?.Load15),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
case 'conns':
|
case 'conns':
|
||||||
return {
|
return {
|
||||||
key,
|
key,
|
||||||
label: '连接',
|
label: '连接',
|
||||||
value: `${props.info.State?.TcpConnCount || 0}|${props.info.State?.UdpConnCount || 0}`,
|
value: `${props.info.State?.TcpConnCount || 0}|${props.info.State?.UdpConnCount || 0}`,
|
||||||
show: true,
|
show: true,
|
||||||
data: {
|
|
||||||
tcp: {
|
|
||||||
value: props.info.State?.TcpConnCount || 0,
|
|
||||||
show: validate.isSet(props.info.State?.TcpConnCount),
|
|
||||||
},
|
|
||||||
udp: {
|
|
||||||
value: props.info.State?.UdpConnCount || 0,
|
|
||||||
show: validate.isSet(props.info.State?.UdpConnCount),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
case 'tcp':
|
case 'tcp':
|
||||||
return {
|
return {
|
||||||
@ -365,7 +292,6 @@ export default (params) => {
|
|||||||
value: props.info.State?.UdpConnCount || 0,
|
value: props.info.State?.UdpConnCount || 0,
|
||||||
show: validate.isSet(props.info.State?.UdpConnCount),
|
show: validate.isSet(props.info.State?.UdpConnCount),
|
||||||
};
|
};
|
||||||
// 入网和出网
|
|
||||||
case 'I-A-O':
|
case 'I-A-O':
|
||||||
return {
|
return {
|
||||||
key,
|
key,
|
||||||
@ -388,7 +314,6 @@ 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 'L-A-P':
|
case 'L-A-P':
|
||||||
return {
|
return {
|
||||||
key,
|
key,
|
||||||
@ -409,7 +334,6 @@ export default (params) => {
|
|||||||
],
|
],
|
||||||
show: validate.isSet(props.info.State?.Load1) || validate.isSet(props.info.State?.ProcessCount),
|
show: validate.isSet(props.info.State?.Load1) || validate.isSet(props.info.State?.ProcessCount),
|
||||||
};
|
};
|
||||||
// 连接 TCP和UDP
|
|
||||||
case 'T-A-U':
|
case 'T-A-U':
|
||||||
return {
|
return {
|
||||||
key,
|
key,
|
||||||
@ -430,7 +354,6 @@ export default (params) => {
|
|||||||
],
|
],
|
||||||
show: validate.isSet(props.info.State?.TcpConnCount) || validate.isSet(props.info.State?.UdpConnCount),
|
show: validate.isSet(props.info.State?.TcpConnCount) || validate.isSet(props.info.State?.UdpConnCount),
|
||||||
};
|
};
|
||||||
// 在线和流量
|
|
||||||
case 'D-A-T':
|
case 'D-A-T':
|
||||||
return {
|
return {
|
||||||
key,
|
key,
|
||||||
|
|||||||
@ -1,128 +0,0 @@
|
|||||||
/**
|
|
||||||
* 服务器排序选项
|
|
||||||
*/
|
|
||||||
export const serverSortOptions = () => [{
|
|
||||||
label: '排序值',
|
|
||||||
value: 'DisplayIndex',
|
|
||||||
}, {
|
|
||||||
label: '主机名称',
|
|
||||||
value: 'Name',
|
|
||||||
}, {
|
|
||||||
label: '国家地区',
|
|
||||||
value: 'Host.CountryCode',
|
|
||||||
}, {
|
|
||||||
label: '系统平台',
|
|
||||||
value: 'Host.Platform',
|
|
||||||
}, {
|
|
||||||
label: '在线时长',
|
|
||||||
value: 'Host.BootTime',
|
|
||||||
}, {
|
|
||||||
label: '入网速度',
|
|
||||||
value: 'State.NetInSpeed',
|
|
||||||
}, {
|
|
||||||
label: '出网速度',
|
|
||||||
value: 'State.NetOutSpeed',
|
|
||||||
}, {
|
|
||||||
label: '入网流量',
|
|
||||||
value: 'State.NetInTransfer',
|
|
||||||
}, {
|
|
||||||
label: '出网流量',
|
|
||||||
value: 'State.NetOutTransfer',
|
|
||||||
}, {
|
|
||||||
label: '合计流量',
|
|
||||||
value: '$.TotalTransfer',
|
|
||||||
}, {
|
|
||||||
label: 'TCP连接',
|
|
||||||
value: 'State.TcpConnCount',
|
|
||||||
}, {
|
|
||||||
label: 'UDP连接',
|
|
||||||
value: 'State.UdpConnCount',
|
|
||||||
}, {
|
|
||||||
label: '总连接数',
|
|
||||||
value: '$.TotalConnCount',
|
|
||||||
}, {
|
|
||||||
label: '1分钟负载',
|
|
||||||
value: 'State.Load1',
|
|
||||||
}, {
|
|
||||||
label: 'CPU占用',
|
|
||||||
value: 'State.CPU',
|
|
||||||
}, {
|
|
||||||
label: '核心数量',
|
|
||||||
value: '$.CPU',
|
|
||||||
}, {
|
|
||||||
label: '内存占用',
|
|
||||||
value: 'State.MemUsed',
|
|
||||||
}, {
|
|
||||||
label: '内存大小',
|
|
||||||
value: 'Host.MemTotal',
|
|
||||||
}, {
|
|
||||||
label: '交换占用',
|
|
||||||
value: 'State.SwapUsed',
|
|
||||||
}, {
|
|
||||||
label: '交换大小',
|
|
||||||
value: 'Host.SwapTotal',
|
|
||||||
}, {
|
|
||||||
label: '硬盘占用',
|
|
||||||
value: 'State.DiskUsed',
|
|
||||||
}, {
|
|
||||||
label: '硬盘大小',
|
|
||||||
value: 'Host.DiskTotal',
|
|
||||||
}];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 服务器排序处理
|
|
||||||
*/
|
|
||||||
export function serverSortHandler(a, b, sortby, order) {
|
|
||||||
let aValue;
|
|
||||||
let bValue;
|
|
||||||
const hasDot = sortby.includes('.');
|
|
||||||
if (!hasDot) {
|
|
||||||
aValue = a[sortby];
|
|
||||||
bValue = b[sortby];
|
|
||||||
} else {
|
|
||||||
const [sortby1, sortby2] = sortby.split('.');
|
|
||||||
if (sortby1 !== '$') {
|
|
||||||
switch (sortby2) {
|
|
||||||
case 'BootTime':
|
|
||||||
{
|
|
||||||
const currentTime = Date.now();
|
|
||||||
aValue = currentTime - a.Host.BootTime * 1000;
|
|
||||||
bValue = currentTime - b.Host.BootTime * 1000;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
{
|
|
||||||
aValue = a[sortby1][sortby2];
|
|
||||||
bValue = b[sortby1][sortby2];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
switch (sortby2) {
|
|
||||||
case 'TotalTransfer':
|
|
||||||
{
|
|
||||||
aValue = a.State.NetInTransfer + a.State.NetOutTransfer;
|
|
||||||
bValue = b.State.NetInTransfer + b.State.NetOutTransfer;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'TotalConnCount':
|
|
||||||
{
|
|
||||||
aValue = a.State.TcpConnCount + a.State.UdpConnCount;
|
|
||||||
bValue = b.State.TcpConnCount + b.State.UdpConnCount;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'CPU':
|
|
||||||
{
|
|
||||||
aValue = a.Host.CPU.length;
|
|
||||||
bValue = b.Host.CPU.length;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (order === 'desc') {
|
|
||||||
return bValue - aValue;
|
|
||||||
}
|
|
||||||
return aValue - bValue;
|
|
||||||
}
|
|
||||||
@ -1,8 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div class="index-container">
|
||||||
class="index-container"
|
|
||||||
:class="indexContainerClass"
|
|
||||||
>
|
|
||||||
<div class="scroll-container">
|
<div class="scroll-container">
|
||||||
<div
|
<div
|
||||||
v-if="worldMapPosition === 'top' && showWorldMap"
|
v-if="worldMapPosition === 'top' && showWorldMap"
|
||||||
@ -15,11 +12,10 @@
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="showFilter"
|
v-if="showFilter"
|
||||||
class="filter-group"
|
class="fitler-group"
|
||||||
:class="{
|
:class="{
|
||||||
'list-is--row': showListRow,
|
'list-is-row': showListRow,
|
||||||
'list-is--card': showListCard,
|
'list-is-card': showListCard,
|
||||||
'list-is--server-status': showListRowByServerStatus,
|
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<div class="left-box">
|
<div class="left-box">
|
||||||
@ -30,11 +26,6 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="right-box">
|
<div class="right-box">
|
||||||
<server-sort-box
|
|
||||||
v-if="showSort"
|
|
||||||
v-model="sortData"
|
|
||||||
:options="sortOptions"
|
|
||||||
/>
|
|
||||||
<server-option-box
|
<server-option-box
|
||||||
v-if="onlineOptions.length"
|
v-if="onlineOptions.length"
|
||||||
v-model="filterFormData.online"
|
v-model="filterFormData.online"
|
||||||
@ -44,12 +35,11 @@
|
|||||||
v-if="config.nazhua.listServerItemTypeToggle"
|
v-if="config.nazhua.listServerItemTypeToggle"
|
||||||
v-model="listType"
|
v-model="listType"
|
||||||
:options="listTypeOptions"
|
:options="listTypeOptions"
|
||||||
:accept-empty="false"
|
:accpet-empty="false"
|
||||||
:mobile-show="false"
|
:mobile-show="false"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- 列表模式 -->
|
|
||||||
<server-list-warp
|
<server-list-warp
|
||||||
v-if="showListRow"
|
v-if="showListRow"
|
||||||
:show-transition="showTransition"
|
:show-transition="showTransition"
|
||||||
@ -61,17 +51,6 @@
|
|||||||
:info="item"
|
:info="item"
|
||||||
/>
|
/>
|
||||||
</server-list-warp>
|
</server-list-warp>
|
||||||
<!-- ServerStatus模式 -->
|
|
||||||
<server-list-warp
|
|
||||||
v-if="showListRowByServerStatus"
|
|
||||||
:show-transition="showTransition"
|
|
||||||
:show-list-by-server-status="showListRowByServerStatus"
|
|
||||||
>
|
|
||||||
<server-status-main
|
|
||||||
:server-list="filterServerList.list"
|
|
||||||
/>
|
|
||||||
</server-list-warp>
|
|
||||||
<!-- 卡片模式 -->
|
|
||||||
<server-list-warp
|
<server-list-warp
|
||||||
v-if="showListCard"
|
v-if="showListCard"
|
||||||
:show-transition="showTransition"
|
:show-transition="showTransition"
|
||||||
@ -127,16 +106,9 @@ import validate from '@/utils/validate';
|
|||||||
|
|
||||||
import WorldMap from '@/components/world-map/world-map.vue';
|
import WorldMap from '@/components/world-map/world-map.vue';
|
||||||
import ServerOptionBox from './components/server-list/server-option-box.vue';
|
import ServerOptionBox from './components/server-list/server-option-box.vue';
|
||||||
import ServerSortBox from './components/server-list/server-sort-box.vue';
|
|
||||||
import ServerListWarp from './components/server-list/server-list-warp.vue';
|
import ServerListWarp from './components/server-list/server-list-warp.vue';
|
||||||
import ServerCardItem from './components/server-list/card/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';
|
import ServerRowItem from './components/server-list/row/server-list-item.vue';
|
||||||
import ServerStatusMain from './components/server-list/server-status/main.vue';
|
|
||||||
|
|
||||||
import {
|
|
||||||
serverSortOptions,
|
|
||||||
serverSortHandler,
|
|
||||||
} from './composable/server-sort';
|
|
||||||
|
|
||||||
const store = useStore();
|
const store = useStore();
|
||||||
const worldMapWidth = ref();
|
const worldMapWidth = ref();
|
||||||
@ -165,39 +137,16 @@ const showListRow = computed(() => {
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
const showListRowByServerStatus = computed(() => {
|
|
||||||
if (windowWidth.value > 1024) {
|
|
||||||
if (config.nazhua.listServerItemTypeToggle) {
|
|
||||||
return listType.value === 'server-status';
|
|
||||||
}
|
|
||||||
return config.nazhua.listServerItemType === 'server-status';
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
const showListCard = computed(() => {
|
const showListCard = computed(() => {
|
||||||
if (windowWidth.value > 1024) {
|
if (windowWidth.value > 1024) {
|
||||||
if (config.nazhua.listServerItemTypeToggle) {
|
if (config.nazhua.listServerItemTypeToggle) {
|
||||||
return listType.value === 'card';
|
return listType.value !== 'row';
|
||||||
}
|
}
|
||||||
return config.nazhua.listServerItemType === 'card';
|
return config.nazhua.listServerItemType !== 'row';
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
const indexContainerClass = computed(() => {
|
|
||||||
const className = {};
|
|
||||||
if (showListRow.value) {
|
|
||||||
className['list-is--row'] = true;
|
|
||||||
}
|
|
||||||
if (showListCard.value) {
|
|
||||||
className['list-is--card'] = true;
|
|
||||||
}
|
|
||||||
if (showListRowByServerStatus.value) {
|
|
||||||
className['list-is--server-status'] = true;
|
|
||||||
}
|
|
||||||
return className;
|
|
||||||
});
|
|
||||||
|
|
||||||
const showFilter = computed(() => config.nazhua.hideFilter !== true);
|
const showFilter = computed(() => config.nazhua.hideFilter !== true);
|
||||||
const filterFormData = ref({
|
const filterFormData = ref({
|
||||||
tag: '',
|
tag: '',
|
||||||
@ -268,31 +217,16 @@ watch(() => serverCount.value, () => {
|
|||||||
|
|
||||||
const listTypeOptions = computed(() => [{
|
const listTypeOptions = computed(() => [{
|
||||||
key: 'card',
|
key: 'card',
|
||||||
label: '卡片模式',
|
label: '卡片',
|
||||||
value: 'card',
|
value: 'card',
|
||||||
icon: 'ri-gallery-view-2',
|
icon: 'ri-gallery-view-2',
|
||||||
}, {
|
}, {
|
||||||
key: 'row',
|
key: 'row',
|
||||||
label: '列表模式',
|
label: '列表',
|
||||||
value: 'row',
|
value: 'row',
|
||||||
icon: 'ri-list-view',
|
icon: 'ri-list-view',
|
||||||
}, {
|
|
||||||
key: 'server-status',
|
|
||||||
label: 'ServerStatus模式',
|
|
||||||
value: 'server-status',
|
|
||||||
icon: 'ri-server-line',
|
|
||||||
}]);
|
}]);
|
||||||
|
|
||||||
/**
|
|
||||||
* 排序处理
|
|
||||||
*/
|
|
||||||
const showSort = computed(() => config.nazhua.hideSort !== true);
|
|
||||||
const sortData = ref({
|
|
||||||
prop: 'DisplayIndex',
|
|
||||||
order: 'desc',
|
|
||||||
});
|
|
||||||
const sortOptions = computed(() => serverSortOptions());
|
|
||||||
|
|
||||||
const filterServerList = computed(() => {
|
const filterServerList = computed(() => {
|
||||||
const fields = {};
|
const fields = {};
|
||||||
const locationMap = {};
|
const locationMap = {};
|
||||||
@ -353,7 +287,6 @@ const filterServerList = computed(() => {
|
|||||||
|
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
list.sort((a, b) => serverSortHandler(a, b, sortData.value.prop, sortData.value.order));
|
|
||||||
return {
|
return {
|
||||||
fields,
|
fields,
|
||||||
list,
|
list,
|
||||||
@ -412,10 +345,7 @@ const worldMapPosition = computed(() => {
|
|||||||
* 处理窗口大小变化
|
* 处理窗口大小变化
|
||||||
*/
|
*/
|
||||||
function handleResize() {
|
function handleResize() {
|
||||||
const serverListContainer = document.querySelector('.server-list-container');
|
worldMapWidth.value = document.querySelector('.server-list-container').clientWidth - 40;
|
||||||
if (serverListContainer) {
|
|
||||||
worldMapWidth.value = serverListContainer.clientWidth - 40;
|
|
||||||
}
|
|
||||||
windowWidth.value = window.innerWidth;
|
windowWidth.value = window.innerWidth;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -452,7 +382,6 @@ onActivated(() => {
|
|||||||
.index-container {
|
.index-container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
.scroll-container {
|
.scroll-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -471,23 +400,9 @@ onActivated(() => {
|
|||||||
.bottom-world-map {
|
.bottom-world-map {
|
||||||
margin-top: 30px;
|
margin-top: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.list-is--server-status {
|
|
||||||
--list-container-width: 1300px;
|
|
||||||
|
|
||||||
// 针对1440px以下的屏幕
|
|
||||||
@media screen and (max-width: 1440px) {
|
|
||||||
--list-container-width: 1300px;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 针对1280px以下的屏幕
|
|
||||||
@media screen and (max-width: 1280px) {
|
|
||||||
--list-container-width: 1200px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter-group {
|
.fitler-group {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import config from '@/config';
|
|||||||
import MessageSubscribe from '@/utils/subscribe';
|
import MessageSubscribe from '@/utils/subscribe';
|
||||||
import v1TransformV0 from '@/utils/transform-v1-2-v0';
|
import v1TransformV0 from '@/utils/transform-v1-2-v0';
|
||||||
|
|
||||||
import WSService, { WS_CONNECTION_STATUS } from './service';
|
import WSService from './service';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取不同版本的WebSocket路径
|
* 获取不同版本的WebSocket路径
|
||||||
@ -50,7 +50,7 @@ const wsService = new WSService({
|
|||||||
});
|
});
|
||||||
|
|
||||||
function restart() {
|
function restart() {
|
||||||
if (wsService.connected !== WS_CONNECTION_STATUS.DISCONNECTED) {
|
if (wsService.connected !== 0) {
|
||||||
wsService.close();
|
wsService.close();
|
||||||
}
|
}
|
||||||
wsService.active();
|
wsService.active();
|
||||||
@ -63,7 +63,7 @@ export {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default (actived) => {
|
export default (actived) => {
|
||||||
if (wsService.connected === WS_CONNECTION_STATUS.CONNECTED) {
|
if (wsService.connected === 2) {
|
||||||
if (actived) {
|
if (actived) {
|
||||||
actived();
|
actived();
|
||||||
}
|
}
|
||||||
@ -75,7 +75,7 @@ export default (actived) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
// 如果已经连接中,则不再连接
|
// 如果已经连接中,则不再连接
|
||||||
if (wsService.connected === WS_CONNECTION_STATUS.CONNECTING) {
|
if (wsService.connected === 1) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
wsService.active();
|
wsService.active();
|
||||||
|
|||||||
@ -1,11 +1,3 @@
|
|||||||
// WebSocket 连接状态常量
|
|
||||||
export const WS_CONNECTION_STATUS = {
|
|
||||||
DISCONNECTED: 0, // 未连接
|
|
||||||
CONNECTING: 1, // 连接中
|
|
||||||
CONNECTED: 2, // 已连接
|
|
||||||
CLOSED: -1, // 已关闭
|
|
||||||
};
|
|
||||||
|
|
||||||
class WSService {
|
class WSService {
|
||||||
constructor(options) {
|
constructor(options) {
|
||||||
const {
|
const {
|
||||||
@ -31,15 +23,16 @@ class WSService {
|
|||||||
messageError: onMessageError || (() => {}),
|
messageError: onMessageError || (() => {}),
|
||||||
};
|
};
|
||||||
|
|
||||||
// 单例模式:防止重复创建 WebSocket 连接
|
// 单例模式 遇到重复的ws服务,不再允许建立新的ws消息处理,如果遇到问题,等待用户自行刷新页面(破罐子破摔解决方法)
|
||||||
// 如果检测到已有实例,触发错误回调并返回,避免资源浪费
|
|
||||||
if (WSService.instance) {
|
if (WSService.instance) {
|
||||||
|
// 抛出错误,防止重复创建 WebSocket 连接
|
||||||
this.$on.error(new Error('WebSocket connection already exists'));
|
this.$on.error(new Error('WebSocket connection already exists'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
WSService.instance = this;
|
WSService.instance = this;
|
||||||
this.connected = WS_CONNECTION_STATUS.DISCONNECTED;
|
// 0: 未连接,1: 连接中,2: 已连接,-1: 已关闭
|
||||||
|
this.connected = 0;
|
||||||
this.ws = undefined;
|
this.ws = undefined;
|
||||||
this.evt = (event) => {
|
this.evt = (event) => {
|
||||||
if (this.debug) {
|
if (this.debug) {
|
||||||
@ -59,18 +52,18 @@ class WSService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get isConnected() {
|
get isConnected() {
|
||||||
return this.connected === WS_CONNECTION_STATUS.CONNECTED;
|
return this.connected === 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
active() {
|
active() {
|
||||||
// 如果已经连接中或已连接,则不再连接
|
// 如果已经连接中或已连接,则不再连接
|
||||||
if (this.connected > WS_CONNECTION_STATUS.DISCONNECTED) {
|
if (this.connected > 0) {
|
||||||
console.warn('WebSocket connection already exists or is connecting');
|
console.warn('WebSocket connection already exists or is connecting');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 标记为正在连接中
|
// 标记为正在连接中
|
||||||
this.connected = WS_CONNECTION_STATUS.CONNECTING;
|
this.connected = 1;
|
||||||
|
|
||||||
// 创建 WebSocket 连接
|
// 创建 WebSocket 连接
|
||||||
this.ws = new WebSocket(this.$wsUrl);
|
this.ws = new WebSocket(this.$wsUrl);
|
||||||
@ -78,14 +71,14 @@ class WSService {
|
|||||||
if (this.debug) {
|
if (this.debug) {
|
||||||
console.log('socket connected', event);
|
console.log('socket connected', event);
|
||||||
}
|
}
|
||||||
this.connected = WS_CONNECTION_STATUS.CONNECTED;
|
this.connected = 2;
|
||||||
this.$on.connect(event);
|
this.$on.connect(event);
|
||||||
});
|
});
|
||||||
this.ws.addEventListener('close', (event) => {
|
this.ws.addEventListener('close', (event) => {
|
||||||
if (this.debug) {
|
if (this.debug) {
|
||||||
console.log('socket closed', event);
|
console.log('socket closed', event);
|
||||||
}
|
}
|
||||||
this.connected = WS_CONNECTION_STATUS.CLOSED;
|
this.connected = -1;
|
||||||
WSService.instance = null; // 清除实例引用
|
WSService.instance = null; // 清除实例引用
|
||||||
this.$on.close(event);
|
this.$on.close(event);
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user