mirror of
https://github.com/hi2shark/nazhua.git
synced 2026-01-19 18:49:36 +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
|
# Nazhua
|
||||||
|
**使用前,请务必阅读Readme的内容,对你有帮助**
|
||||||
基于哪吒监控(nezha.wiki)v0版本构建的前端主题,目前兼容与v0相同数据结构的v1版本。
|
基于哪吒监控(nezha.wiki)v0版本构建的前端主题,目前兼容与v0相同数据结构的v1版本。
|
||||||
~~主题有点**重**,因为内置了一个带中文的`SarasaTermSC-SemiBold`字体。~~
|
~~主题有点**重**,因为内置了一个带中文的`SarasaTermSC-SemiBold`字体。~~
|
||||||
根据不同场景,可以选择是否打包带入或者是否加载这个字体。
|
根据不同场景,可以选择是否打包带入或者是否加载这个字体。
|
||||||
@ -30,7 +31,9 @@ V0下载最新版本[Releases](https://github.com/hi2shark/nazhua/releases)的`v
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
对于几个我常见的国别位置,添加了默认映射位置,会自动显示在地图上。(美国太大了,就默认显示在最常买的位置:洛杉矶)
|
对于几个我常见的国别位置,添加了默认映射位置,会自动显示在地图上。
|
||||||
|
Tips: 中国大陆地区默认在首都:北京(该映射在0.4.6后补充)
|
||||||
|
Tips: 美国默认在最常买的位置:洛杉矶
|
||||||
|
|
||||||
## 关于节点slogan和购买链接
|
## 关于节点slogan和购买链接
|
||||||
同时,这个`customData`中还可以添加一项`slogan`和`orderLink`字符串,分别用于显示节点的标语和购买链接。
|
同时,这个`customData`中还可以添加一项`slogan`和`orderLink`字符串,分别用于显示节点的标语和购买链接。
|
||||||
@ -107,6 +110,11 @@ services:
|
|||||||
**如果不想加载完整的内置库,可以使用cdn引用镜像**
|
**如果不想加载完整的内置库,可以使用cdn引用镜像**
|
||||||
例如:`ghcr.io/hi2shark/nazhua:latest`替换为`ghcr.io/hi2shark/nazhua: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配置示例
|
||||||
```nginx
|
```nginx
|
||||||
map $http_upgrade $connection_upgrade {
|
map $http_upgrade $connection_upgrade {
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
<div
|
<div
|
||||||
ref="pointRef"
|
ref="pointRef"
|
||||||
class="world-map-point"
|
class="world-map-point"
|
||||||
|
:class="'world-map-point--' + (info?.type || 'default')"
|
||||||
:style="pointStyle"
|
:style="pointStyle"
|
||||||
:title="info?.label || ''"
|
:title="info?.label || ''"
|
||||||
@click="handleClick"
|
@click="handleClick"
|
||||||
@ -88,5 +89,29 @@ function handleClick() {
|
|||||||
@media screen and (max-width: 720px) {
|
@media screen and (max-width: 720px) {
|
||||||
--map-point-scale: 0.5;
|
--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>
|
</style>
|
||||||
|
|||||||
@ -37,10 +37,14 @@
|
|||||||
import {
|
import {
|
||||||
ref,
|
ref,
|
||||||
computed,
|
computed,
|
||||||
|
watch,
|
||||||
} from 'vue';
|
} from 'vue';
|
||||||
import validate from '@/utils/validate';
|
import validate from '@/utils/validate';
|
||||||
|
|
||||||
import WorldMapPoint from './world-map-point.vue';
|
import WorldMapPoint from './world-map-point.vue';
|
||||||
|
import {
|
||||||
|
findIntersectingGroups,
|
||||||
|
} from '@/utils/world-map';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
width: {
|
width: {
|
||||||
@ -100,16 +104,75 @@ const mapStyle = computed(() => {
|
|||||||
return style;
|
return style;
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapPoints = computed(() => props.locations.map((i) => {
|
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 = {
|
const item = {
|
||||||
key: i.key,
|
key: i.key,
|
||||||
left: (computedSize.value.width / 1280) * i.x,
|
left: (computedSize.value.width / 1280) * i.x,
|
||||||
top: (computedSize.value.height / 621) * i.y,
|
top: (computedSize.value.height / 621) * i.y,
|
||||||
size: i.size || 4,
|
size: i.size || 4,
|
||||||
label: i.label,
|
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;
|
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) {
|
if (window.innerWidth > 500) {
|
||||||
style.top = `${activeTipsXY.value.y}px`;
|
style.top = `${activeTipsXY.value.y}px`;
|
||||||
style.left = `${activeTipsXY.value.x}px`;
|
style.left = `${activeTipsXY.value.x}px`;
|
||||||
style.transform = 'translate(-50%, 100%)';
|
style.transform = 'translate(-50%, 20px)';
|
||||||
} else {
|
} else {
|
||||||
style.bottom = '10px';
|
style.bottom = '10px';
|
||||||
style.left = '50%';
|
style.left = '50%';
|
||||||
@ -133,18 +196,18 @@ const tipsContentStyle = computed(() => {
|
|||||||
}
|
}
|
||||||
return style;
|
return style;
|
||||||
});
|
});
|
||||||
let timer = null;
|
let handlePointTapTimer = null;
|
||||||
function handlePointTap(e) {
|
function handlePointTap(e) {
|
||||||
tipsContent.value = e.label;
|
tipsContent.value = e.label;
|
||||||
activeTipsXY.value = {
|
activeTipsXY.value = {
|
||||||
x: e.left - (e.size / 2),
|
x: e.left,
|
||||||
y: e.top - e.size,
|
y: e.top - 10,
|
||||||
};
|
};
|
||||||
tipsShow.value = true;
|
tipsShow.value = true;
|
||||||
if (timer) {
|
if (handlePointTapTimer) {
|
||||||
clearTimeout(timer);
|
clearTimeout(handlePointTapTimer);
|
||||||
}
|
}
|
||||||
timer = setTimeout(() => {
|
handlePointTapTimer = setTimeout(() => {
|
||||||
tipsShow.value = false;
|
tipsShow.value = false;
|
||||||
}, 5000);
|
}, 5000);
|
||||||
}
|
}
|
||||||
@ -173,6 +236,20 @@ function handlePointTap(e) {
|
|||||||
background: rgba(#000, 0.8);
|
background: rgba(#000, 0.8);
|
||||||
box-shadow: 1px 4px 8px rgba(#303841, 0.4);
|
box-shadow: 1px 4px 8px rgba(#303841, 0.4);
|
||||||
z-index: 100;
|
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 = {
|
export const countryCodeMapping = {
|
||||||
|
CN: 'PEK',
|
||||||
JP: 'TYO',
|
JP: 'TYO',
|
||||||
SG: 'SIN',
|
SG: 'SIN',
|
||||||
KR: 'SEL',
|
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 {
|
import {
|
||||||
alias2code,
|
alias2code,
|
||||||
locationCode2Info,
|
locationCode2Info,
|
||||||
} from '@/utils/world-map-location';
|
} from '@/utils/world-map';
|
||||||
|
|
||||||
import WorldMap from '@/components/world-map/world-map.vue';
|
import WorldMap from '@/components/world-map/world-map.vue';
|
||||||
import ServerName from './components/server-detail/server-name.vue';
|
import ServerName from './components/server-detail/server-name.vue';
|
||||||
@ -99,6 +99,7 @@ const locations = computed(() => {
|
|||||||
code,
|
code,
|
||||||
size: 4,
|
size: 4,
|
||||||
label: `${name}`,
|
label: `${name}`,
|
||||||
|
servers: [info.value],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return arr;
|
return arr;
|
||||||
|
|||||||
@ -64,7 +64,7 @@ import {
|
|||||||
alias2code,
|
alias2code,
|
||||||
locationCode2Info,
|
locationCode2Info,
|
||||||
count2size,
|
count2size,
|
||||||
} from '@/utils/world-map-location';
|
} from '@/utils/world-map';
|
||||||
import uuid from '@/utils/uuid';
|
import uuid from '@/utils/uuid';
|
||||||
|
|
||||||
import WorldMap from '@/components/world-map/world-map.vue';
|
import WorldMap from '@/components/world-map/world-map.vue';
|
||||||
@ -149,13 +149,13 @@ const serverLocations = computed(() => {
|
|||||||
const code = alias2code(aliasCode) || locationCode;
|
const code = alias2code(aliasCode) || locationCode;
|
||||||
if (code) {
|
if (code) {
|
||||||
if (!locationMap[code]) {
|
if (!locationMap[code]) {
|
||||||
locationMap[code] = 0;
|
locationMap[code] = [];
|
||||||
}
|
}
|
||||||
locationMap[code] += 1;
|
locationMap[code].push(i);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const locations = [];
|
const locations = [];
|
||||||
Object.entries(locationMap).forEach(([code, count]) => {
|
Object.entries(locationMap).forEach(([code, servers]) => {
|
||||||
const {
|
const {
|
||||||
x,
|
x,
|
||||||
y,
|
y,
|
||||||
@ -167,8 +167,9 @@ const serverLocations = computed(() => {
|
|||||||
x,
|
x,
|
||||||
y,
|
y,
|
||||||
code,
|
code,
|
||||||
size: count2size(count),
|
size: count2size(servers.length),
|
||||||
label: `${name},${count}台`,
|
label: `${name},${servers.length}台`,
|
||||||
|
servers,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user