mirror of
https://github.com/hi2shark/nazhua.git
synced 2026-01-17 01:30:44 +08:00
Compare commits
4 Commits
62b9c497cb
...
92faaed431
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
92faaed431 | ||
|
|
eaabab623a | ||
|
|
20281a1848 | ||
|
|
b825894796 |
10
readme.md
10
readme.md
@ -1,4 +1,5 @@
|
||||
# Nazhua
|
||||
**使用前,请务必阅读Readme的内容,对你有帮助**
|
||||
基于哪吒监控(nezha.wiki)v0版本构建的前端主题,目前兼容与v0相同数据结构的v1版本。
|
||||
~~主题有点**重**,因为内置了一个带中文的`SarasaTermSC-SemiBold`字体。~~
|
||||
根据不同场景,可以选择是否打包带入或者是否加载这个字体。
|
||||
@ -30,7 +31,9 @@ V0下载最新版本[Releases](https://github.com/hi2shark/nazhua/releases)的`v
|
||||
}
|
||||
}
|
||||
```
|
||||
对于几个我常见的国别位置,添加了默认映射位置,会自动显示在地图上。(美国太大了,就默认显示在最常买的位置:洛杉矶)
|
||||
对于几个我常见的国别位置,添加了默认映射位置,会自动显示在地图上。
|
||||
Tips: 中国大陆地区默认在首都:北京(该映射在0.4.6后补充)
|
||||
Tips: 美国默认在最常买的位置:洛杉矶
|
||||
|
||||
## 关于节点slogan和购买链接
|
||||
同时,这个`customData`中还可以添加一项`slogan`和`orderLink`字符串,分别用于显示节点的标语和购买链接。
|
||||
@ -107,6 +110,11 @@ services:
|
||||
**如果不想加载完整的内置库,可以使用cdn引用镜像**
|
||||
例如:`ghcr.io/hi2shark/nazhua:latest`替换为`ghcr.io/hi2shark/nazhua:cdn`
|
||||
|
||||
>如果你想隐藏原面板,只暴露nazhua出来,你可以用Zero Trust的Tunnels;
|
||||
>三个容器:Tunnels、nezha-dashboard、nazhua
|
||||
>nazhua用docker内的地址访问nezha-dashboard,然后Tunnels绑定nazhua给公开访问的域名
|
||||
>Tunnels绑定nezha-dashboard到私密域名,需要邮箱|IP等匹配的才能访问
|
||||
|
||||
### Nginx配置示例
|
||||
```nginx
|
||||
map $http_upgrade $connection_upgrade {
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
<div
|
||||
ref="pointRef"
|
||||
class="world-map-point"
|
||||
:class="'world-map-point--' + (info?.type || 'default')"
|
||||
:style="pointStyle"
|
||||
:title="info?.label || ''"
|
||||
@click="handleClick"
|
||||
@ -88,5 +89,29 @@ function handleClick() {
|
||||
@media screen and (max-width: 720px) {
|
||||
--map-point-scale: 0.5;
|
||||
}
|
||||
|
||||
&--group {
|
||||
.point-block {
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: calc(var(--map-point-size) * var(--map-point-scale) + (16px * var(--map-point-scale)));
|
||||
height: calc(var(--map-point-size) * var(--map-point-scale) + (16px * var(--map-point-scale)));
|
||||
transform: translate(-50%, -50%);
|
||||
border: calc(2px * var(--map-point-scale)) solid var(--world-map-point-color);
|
||||
border-radius: 50%;
|
||||
opacity: 0.7;
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
&::after {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -37,10 +37,14 @@
|
||||
import {
|
||||
ref,
|
||||
computed,
|
||||
watch,
|
||||
} from 'vue';
|
||||
import validate from '@/utils/validate';
|
||||
|
||||
import WorldMapPoint from './world-map-point.vue';
|
||||
import {
|
||||
findIntersectingGroups,
|
||||
} from '@/utils/world-map';
|
||||
|
||||
const props = defineProps({
|
||||
width: {
|
||||
@ -100,16 +104,75 @@ const mapStyle = computed(() => {
|
||||
return style;
|
||||
});
|
||||
|
||||
const mapPoints = computed(() => props.locations.map((i) => {
|
||||
const item = {
|
||||
key: i.key,
|
||||
left: (computedSize.value.width / 1280) * i.x,
|
||||
top: (computedSize.value.height / 621) * i.y,
|
||||
size: i.size || 4,
|
||||
label: i.label,
|
||||
};
|
||||
return item;
|
||||
}));
|
||||
const mapPoints = ref([]);
|
||||
let computeMapPointsTimer = null;
|
||||
function computeMapPoints() {
|
||||
if (computeMapPointsTimer) {
|
||||
clearTimeout(computeMapPointsTimer);
|
||||
}
|
||||
if (props.locations.length === 0) {
|
||||
mapPoints.value = [];
|
||||
return;
|
||||
}
|
||||
computeMapPointsTimer = setTimeout(() => {
|
||||
const points = props.locations.map((i) => {
|
||||
const item = {
|
||||
key: i.key,
|
||||
left: (computedSize.value.width / 1280) * i.x,
|
||||
top: (computedSize.value.height / 621) * i.y,
|
||||
size: i.size || 4,
|
||||
label: i.label,
|
||||
servers: i.servers,
|
||||
type: 'single',
|
||||
};
|
||||
const halfSize = (item.size + 8) / 2;
|
||||
item.topLeft = {
|
||||
left: item.left - halfSize,
|
||||
top: item.top - halfSize,
|
||||
};
|
||||
item.bottomRight = {
|
||||
left: item.left + halfSize,
|
||||
top: item.top + halfSize,
|
||||
};
|
||||
return item;
|
||||
});
|
||||
const groups = findIntersectingGroups(points);
|
||||
Object.entries(groups).forEach(([key, group]) => {
|
||||
const item = points.find((i) => i.key === key);
|
||||
if (item.parent) {
|
||||
return;
|
||||
}
|
||||
item.size = 4;
|
||||
item.type = 'group';
|
||||
item.children = group;
|
||||
let label = item.label || '';
|
||||
let servers = [...(item.servers || [])];
|
||||
group.forEach((i) => {
|
||||
if (!i.parent && !i.children) {
|
||||
i.parent = item;
|
||||
label += `\n${i.label}`;
|
||||
servers = servers.concat((i.servers || []));
|
||||
}
|
||||
});
|
||||
item.label = label;
|
||||
item.servers = servers;
|
||||
});
|
||||
mapPoints.value = points.filter((i) => !i.parent);
|
||||
}, 100);
|
||||
}
|
||||
|
||||
watch(() => props.locations, () => {
|
||||
computeMapPoints();
|
||||
}, {
|
||||
immediate: true,
|
||||
});
|
||||
|
||||
watch(() => computedSize.value, () => {
|
||||
computeMapPoints();
|
||||
}, {
|
||||
immediate: true,
|
||||
deep: true,
|
||||
});
|
||||
|
||||
/**
|
||||
* 提示框
|
||||
@ -125,7 +188,7 @@ const tipsContentStyle = computed(() => {
|
||||
if (window.innerWidth > 500) {
|
||||
style.top = `${activeTipsXY.value.y}px`;
|
||||
style.left = `${activeTipsXY.value.x}px`;
|
||||
style.transform = 'translate(-50%, 100%)';
|
||||
style.transform = 'translate(-50%, 20px)';
|
||||
} else {
|
||||
style.bottom = '10px';
|
||||
style.left = '50%';
|
||||
@ -133,18 +196,18 @@ const tipsContentStyle = computed(() => {
|
||||
}
|
||||
return style;
|
||||
});
|
||||
let timer = null;
|
||||
let handlePointTapTimer = null;
|
||||
function handlePointTap(e) {
|
||||
tipsContent.value = e.label;
|
||||
activeTipsXY.value = {
|
||||
x: e.left - (e.size / 2),
|
||||
y: e.top - e.size,
|
||||
x: e.left,
|
||||
y: e.top - 10,
|
||||
};
|
||||
tipsShow.value = true;
|
||||
if (timer) {
|
||||
clearTimeout(timer);
|
||||
if (handlePointTapTimer) {
|
||||
clearTimeout(handlePointTapTimer);
|
||||
}
|
||||
timer = setTimeout(() => {
|
||||
handlePointTapTimer = setTimeout(() => {
|
||||
tipsShow.value = false;
|
||||
}, 5000);
|
||||
}
|
||||
@ -173,6 +236,20 @@ function handlePointTap(e) {
|
||||
background: rgba(#000, 0.8);
|
||||
box-shadow: 1px 4px 8px rgba(#303841, 0.4);
|
||||
z-index: 100;
|
||||
white-space: pre;
|
||||
|
||||
// 向上的尖角
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 100%;
|
||||
left: 50%;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border: 5px solid transparent;
|
||||
border-bottom-color: rgba(#000, 0.8);
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -218,6 +218,7 @@ export const aliasMapping = {
|
||||
};
|
||||
|
||||
export const countryCodeMapping = {
|
||||
CN: 'PEK',
|
||||
JP: 'TYO',
|
||||
SG: 'SIN',
|
||||
KR: 'SEL',
|
||||
|
||||
@ -1,35 +0,0 @@
|
||||
import config from '@/config';
|
||||
import CODE_MAPS, {
|
||||
countryCodeMapping,
|
||||
aliasMapping,
|
||||
} from '@/data/code-maps';
|
||||
|
||||
export const ALIAS_CODE = {
|
||||
...aliasMapping,
|
||||
...countryCodeMapping,
|
||||
};
|
||||
|
||||
export const alias2code = (code) => ALIAS_CODE[code];
|
||||
|
||||
export const locationCode2Info = (code) => {
|
||||
const maps = {
|
||||
...CODE_MAPS,
|
||||
...(config.nazhua.customCodeMap || {}),
|
||||
};
|
||||
let info = maps[code];
|
||||
const aliasCode = aliasMapping[code];
|
||||
if (!info && aliasCode) {
|
||||
info = maps[aliasCode];
|
||||
}
|
||||
return info;
|
||||
};
|
||||
|
||||
export const count2size = (count) => {
|
||||
if (count < 3) {
|
||||
return 4;
|
||||
}
|
||||
if (count < 5) {
|
||||
return 6;
|
||||
}
|
||||
return 8;
|
||||
};
|
||||
61
src/utils/world-map.js
Normal file
61
src/utils/world-map.js
Normal file
@ -0,0 +1,61 @@
|
||||
import config from '@/config';
|
||||
import CODE_MAPS, {
|
||||
countryCodeMapping,
|
||||
aliasMapping,
|
||||
} from '@/data/code-maps';
|
||||
|
||||
export const ALIAS_CODE = {
|
||||
...aliasMapping,
|
||||
...countryCodeMapping,
|
||||
};
|
||||
|
||||
export const alias2code = (code) => ALIAS_CODE[code];
|
||||
|
||||
export const locationCode2Info = (code) => {
|
||||
const maps = {
|
||||
...CODE_MAPS,
|
||||
...(config.nazhua.customCodeMap || {}),
|
||||
};
|
||||
let info = maps[code];
|
||||
const aliasCode = aliasMapping[code];
|
||||
if (!info && aliasCode) {
|
||||
info = maps[aliasCode];
|
||||
}
|
||||
return info;
|
||||
};
|
||||
|
||||
export const count2size = (count) => {
|
||||
if (count < 3) {
|
||||
return 4;
|
||||
}
|
||||
if (count < 5) {
|
||||
return 6;
|
||||
}
|
||||
return 8;
|
||||
};
|
||||
|
||||
export function findIntersectingGroups(coordinates) {
|
||||
const groups = {};
|
||||
|
||||
coordinates.forEach((coordinate, index) => {
|
||||
const intersects = [];
|
||||
const n = 2;
|
||||
coordinates.forEach((otherCoordinate, otherIndex) => {
|
||||
if (index !== otherIndex) {
|
||||
if (
|
||||
coordinate.topLeft.top - otherCoordinate.bottomRight.top < n
|
||||
&& coordinate.topLeft.left - otherCoordinate.bottomRight.left < n
|
||||
&& coordinate.bottomRight.top - otherCoordinate.topLeft.top > -n
|
||||
&& coordinate.bottomRight.left - otherCoordinate.topLeft.left > -n
|
||||
) {
|
||||
intersects.push(otherCoordinate);
|
||||
}
|
||||
}
|
||||
});
|
||||
if (intersects.length > 0) {
|
||||
groups[coordinate.key] = intersects;
|
||||
}
|
||||
});
|
||||
|
||||
return groups;
|
||||
}
|
||||
@ -53,7 +53,7 @@ import config from '@/config';
|
||||
import {
|
||||
alias2code,
|
||||
locationCode2Info,
|
||||
} from '@/utils/world-map-location';
|
||||
} from '@/utils/world-map';
|
||||
|
||||
import WorldMap from '@/components/world-map/world-map.vue';
|
||||
import ServerName from './components/server-detail/server-name.vue';
|
||||
@ -99,6 +99,7 @@ const locations = computed(() => {
|
||||
code,
|
||||
size: 4,
|
||||
label: `${name}`,
|
||||
servers: [info.value],
|
||||
});
|
||||
}
|
||||
return arr;
|
||||
|
||||
@ -64,7 +64,7 @@ import {
|
||||
alias2code,
|
||||
locationCode2Info,
|
||||
count2size,
|
||||
} from '@/utils/world-map-location';
|
||||
} from '@/utils/world-map';
|
||||
import uuid from '@/utils/uuid';
|
||||
|
||||
import WorldMap from '@/components/world-map/world-map.vue';
|
||||
@ -149,13 +149,13 @@ const serverLocations = computed(() => {
|
||||
const code = alias2code(aliasCode) || locationCode;
|
||||
if (code) {
|
||||
if (!locationMap[code]) {
|
||||
locationMap[code] = 0;
|
||||
locationMap[code] = [];
|
||||
}
|
||||
locationMap[code] += 1;
|
||||
locationMap[code].push(i);
|
||||
}
|
||||
});
|
||||
const locations = [];
|
||||
Object.entries(locationMap).forEach(([code, count]) => {
|
||||
Object.entries(locationMap).forEach(([code, servers]) => {
|
||||
const {
|
||||
x,
|
||||
y,
|
||||
@ -167,8 +167,9 @@ const serverLocations = computed(() => {
|
||||
x,
|
||||
y,
|
||||
code,
|
||||
size: count2size(count),
|
||||
label: `${name},${count}台`,
|
||||
size: count2size(servers.length),
|
||||
label: `${name},${servers.length}台`,
|
||||
servers,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user