mirror of
https://github.com/hi2shark/nazhua.git
synced 2026-01-18 02:00:43 +08:00
Compare commits
183 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5c347cc0eb | ||
|
|
881c9a05e5 | ||
|
|
586f1dd063 | ||
|
|
93f66cb42c | ||
|
|
0b9da8fe01 | ||
|
|
90884c2730 | ||
|
|
69ab11babc | ||
|
|
33f1625ab1 | ||
|
|
015ce40586 | ||
|
|
da35150a8d | ||
|
|
cf1e7cf9fd | ||
|
|
c8fddc4803 | ||
|
|
ec3b5cb5ea | ||
|
|
640fd787a2 | ||
|
|
2d331823b9 | ||
|
|
0a33582541 | ||
|
|
9aaa5b0cc3 | ||
|
|
eed7be4b1b | ||
|
|
1b20505ef2 | ||
|
|
51197a1c05 | ||
|
|
a0e066c04f | ||
|
|
513a88d37e | ||
|
|
203b86e0e6 | ||
|
|
230bbcf7f0 | ||
|
|
2708263440 | ||
|
|
8dc1aa0154 | ||
|
|
53cd629119 | ||
|
|
3bb971479f | ||
|
|
36d5a6ce66 | ||
|
|
a24401a87c | ||
|
|
8de81431ca | ||
|
|
280387fba5 | ||
|
|
d9ef0b9b84 | ||
|
|
b16fd1ac15 | ||
|
|
d99225eca7 | ||
|
|
d898b58c15 | ||
|
|
59be8baee3 | ||
|
|
d7d4112e70 | ||
|
|
bc50b78135 | ||
|
|
48d6e5c36a | ||
|
|
842cc7d2f8 | ||
|
|
31e9f61384 | ||
|
|
d11deece54 | ||
|
|
39fc6b2497 | ||
|
|
6d3139094f | ||
|
|
873d6a5f28 | ||
|
|
3c4d7b71c2 | ||
|
|
14c83386e7 | ||
|
|
2344200815 | ||
|
|
13d66010df | ||
|
|
f446221f45 | ||
|
|
068c7e09e9 | ||
|
|
9f2c90c5fa | ||
|
|
84dc786b28 | ||
|
|
fbc3937b84 | ||
|
|
ad1b53786e | ||
|
|
3d6e25d352 | ||
|
|
17e548abb1 | ||
|
|
b6d8457649 | ||
|
|
bc5db4b5b1 | ||
|
|
0fea4ee186 | ||
|
|
dbdd1d36ee | ||
|
|
73387bdb79 | ||
|
|
2f1ca9cb0f | ||
|
|
0d43597346 | ||
|
|
25bcbbadc8 | ||
|
|
8d7c815461 | ||
|
|
25c68fa64e | ||
|
|
85ff1a9844 | ||
|
|
2a05809c9c | ||
|
|
86b45b5f2a | ||
|
|
3ede341f3d | ||
|
|
d3e549cad0 | ||
|
|
1f7e87c28d | ||
|
|
26de335304 | ||
|
|
efbf38738f | ||
|
|
9d301b9681 | ||
|
|
786d6c0a87 | ||
|
|
bdbd083d45 | ||
|
|
2910c2bf41 | ||
|
|
bcfc53b784 | ||
|
|
963c06dfce | ||
|
|
718b0138b0 | ||
|
|
c835cec98c | ||
|
|
61fe45927e | ||
|
|
7a2e22c30c | ||
|
|
93c825bf33 | ||
|
|
1974cb4e63 | ||
|
|
bf30e14c30 | ||
|
|
582b367088 | ||
|
|
d10386e6e9 | ||
|
|
836fddf860 | ||
|
|
68bc396ea5 | ||
|
|
3501483af0 | ||
|
|
5ec81d7616 | ||
|
|
26ed6e0722 | ||
|
|
f8cef00bfd | ||
|
|
ead834dd95 | ||
|
|
23e0c515e5 | ||
|
|
482e29579a | ||
|
|
b235be5fa1 | ||
|
|
91bc02e553 | ||
|
|
97e6e0e7a7 | ||
|
|
18df1e3cec | ||
|
|
825c655185 | ||
|
|
69f7031c0d | ||
|
|
6a92673e3a | ||
|
|
8a53dcbb0f | ||
|
|
95d1d72cc7 | ||
|
|
a321ce2f69 | ||
|
|
5a771a4932 | ||
|
|
d580f5fc81 | ||
|
|
7e416a6f16 | ||
|
|
d134e7f2a3 | ||
|
|
ca21559064 | ||
|
|
4d57c3d8f1 | ||
|
|
dfc3d9e750 | ||
|
|
633ba25b7f | ||
|
|
ff751484f4 | ||
|
|
44c0c1651b | ||
|
|
b7523012ec | ||
|
|
0606ba918d | ||
|
|
249a1463b3 | ||
|
|
7d424ffa78 | ||
|
|
057e0f5f11 | ||
|
|
247115c3c3 | ||
|
|
bd6b3c8b44 | ||
|
|
a23fbe4546 | ||
|
|
d85eeb9a44 | ||
|
|
b0365db2bb | ||
|
|
c371755149 | ||
|
|
4502683cd5 | ||
|
|
fb3267f978 | ||
|
|
7bbd8455e9 | ||
|
|
027215720e | ||
|
|
b41a7d34cd | ||
|
|
557951ede3 | ||
|
|
b001277932 | ||
|
|
444b70af47 | ||
|
|
56959c7733 | ||
|
|
aee3c5634b | ||
|
|
71eba02126 | ||
|
|
998955fa31 | ||
|
|
6558f2ffe9 | ||
|
|
8e6489ba5c | ||
|
|
6f1f4d22f0 | ||
|
|
597fdc668e | ||
|
|
d05ff19443 | ||
|
|
724d0d10e0 | ||
|
|
6687b92578 | ||
|
|
b11ac269be | ||
|
|
e18f4f7e2d | ||
|
|
6a238ee0f9 | ||
|
|
ee0928cbe9 | ||
|
|
ffd8a87854 | ||
|
|
1800486d24 | ||
|
|
a1676c9b3e | ||
|
|
0875e12d21 | ||
|
|
4126958d54 | ||
|
|
4e60a52749 | ||
|
|
92faaed431 | ||
|
|
eaabab623a | ||
|
|
20281a1848 | ||
|
|
b825894796 | ||
|
|
62b9c497cb | ||
|
|
11da876d8c | ||
|
|
5e14207302 | ||
|
|
31a8c8a50c | ||
|
|
51669134c9 | ||
|
|
77e177ab39 | ||
|
|
4e2ff5b84e | ||
|
|
70dbd77f19 | ||
|
|
1108b82510 | ||
|
|
66251d3f57 | ||
|
|
8f19ca40d0 | ||
|
|
ca50ac15c7 | ||
|
|
5666efa542 | ||
|
|
5b2bea95a0 | ||
|
|
f2fb191b52 | ||
|
|
5f719d5e5a | ||
|
|
b8e08f3dd1 | ||
|
|
c632f55ccb | ||
|
|
1c6107cb07 |
15
.env
15
.env
@ -1,4 +1,11 @@
|
|||||||
# WS_HOST=
|
#### Sarasa Term SC字体的配置
|
||||||
# API_HOST=
|
# VITE_DISABLE_SARASA_TERM_SC=1
|
||||||
# NEZHA_HOST=
|
# VITE_SARASA_TERM_SC_USE_CDN=1
|
||||||
# NEZHA_HOST_REPACE_PATH=
|
|
||||||
|
#### 引用库的CDN配置
|
||||||
|
# VITE_USE_CDN=1
|
||||||
|
# VITE_CDN_LIB_TYPE=jsdelivr # jsdelivr | cdnjs | loli
|
||||||
|
|
||||||
|
#### 哪吒的默认版本控制
|
||||||
|
# VITE_NEZHA_VERSION=v1 # v0 | v0
|
||||||
|
|
||||||
|
|||||||
11
.env.development.local.template
Normal file
11
.env.development.local.template
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
# 能反代WS的主机,方便本地ws调试
|
||||||
|
PROXY_WS_HOST=
|
||||||
|
# API的HOST,/api/xxx会被替换为这个HOST
|
||||||
|
API_HOST=
|
||||||
|
# WS的HOST,/ws和/api/v1/ws/server会被替换为这个HOST
|
||||||
|
WS_HOST=
|
||||||
|
# V0开发模式需要一个哪吒地址,用去抓取哪吒的数据
|
||||||
|
NEZHA_HOST=
|
||||||
|
# VO开发模式,本地会请求/nezha/,此项为1时,会去掉/nezha/
|
||||||
|
NEZHA_HOST_REPACE_PATH=1
|
||||||
|
|
||||||
@ -66,6 +66,7 @@ module.exports = {
|
|||||||
'no-param-reassign': 'off',
|
'no-param-reassign': 'off',
|
||||||
'no-underscore-dangle': 'off',
|
'no-underscore-dangle': 'off',
|
||||||
'no-unsafe-optional-chaining': 'off',
|
'no-unsafe-optional-chaining': 'off',
|
||||||
|
'max-classes-per-file': 'off',
|
||||||
'max-len': ['warn', 120],
|
'max-len': ['warn', 120],
|
||||||
'vue/max-len': ['warn', 120],
|
'vue/max-len': ['warn', 120],
|
||||||
'object-property-newline': ['error', {
|
'object-property-newline': ['error', {
|
||||||
|
|||||||
BIN
.github/images/nazhua-detail-mobile.webp
vendored
Normal file
BIN
.github/images/nazhua-detail-mobile.webp
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
BIN
.github/images/nazhua-main.webp
vendored
Normal file
BIN
.github/images/nazhua-main.webp
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 133 KiB |
BIN
.github/images/nazhua-mobile.webp
vendored
Normal file
BIN
.github/images/nazhua-mobile.webp
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
BIN
.github/images/remixicon-select.jpg
vendored
Normal file
BIN
.github/images/remixicon-select.jpg
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
BIN
.github/images/vmiss-logo.jpg
vendored
Normal file
BIN
.github/images/vmiss-logo.jpg
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
BIN
.github/images/yxvm-logo.jpg
vendored
Normal file
BIN
.github/images/yxvm-logo.jpg
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.6 KiB |
26
.github/workflows/docker-build.yml
vendored
26
.github/workflows/docker-build.yml
vendored
@ -1,6 +1,9 @@
|
|||||||
name: Build and Push Docker Image
|
name: Build and Push Docker Image
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*.*.*'
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
version:
|
version:
|
||||||
@ -9,7 +12,7 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-24.04
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
@ -35,13 +38,24 @@ jobs:
|
|||||||
- name: Print version
|
- name: Print version
|
||||||
run: echo "Version is $VERSION"
|
run: echo "Version is $VERSION"
|
||||||
|
|
||||||
- name: Build project
|
- name: 构建完整引用版本
|
||||||
run: npm run build
|
run: npm run build
|
||||||
|
|
||||||
- name: Build Docker image
|
- name: 构建完整引用版本的Docker镜像
|
||||||
run: |
|
run: |
|
||||||
docker build -t ghcr.io/${{ github.repository }}:$VERSION .
|
docker build -t ghcr.io/${{ github.repository }}:$VERSION .
|
||||||
|
|
||||||
|
- name: 构建CDN引用版本
|
||||||
|
env:
|
||||||
|
VITE_SARASA_TERM_SC_USE_CDN: '1'
|
||||||
|
VITE_USE_CDN: '1'
|
||||||
|
VITE_CDN_LIB_TYPE: 'loli'
|
||||||
|
run: npm run build
|
||||||
|
|
||||||
|
- name: 构建CDN引用版本的Docker镜像
|
||||||
|
run: |
|
||||||
|
docker build -t ghcr.io/${{ github.repository }}:$VERSION-cdn .
|
||||||
|
|
||||||
- name: Log in to GitHub Container Registry
|
- name: Log in to GitHub Container Registry
|
||||||
run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
|
run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
|
||||||
|
|
||||||
@ -50,3 +64,9 @@ jobs:
|
|||||||
docker push ghcr.io/${{ github.repository }}:$VERSION
|
docker push ghcr.io/${{ github.repository }}:$VERSION
|
||||||
docker tag ghcr.io/${{ github.repository }}:$VERSION ghcr.io/${{ github.repository }}:latest
|
docker tag ghcr.io/${{ github.repository }}:$VERSION ghcr.io/${{ github.repository }}:latest
|
||||||
docker push ghcr.io/${{ github.repository }}:latest
|
docker push ghcr.io/${{ github.repository }}:latest
|
||||||
|
|
||||||
|
- name: Push CDN Docker image
|
||||||
|
run: |
|
||||||
|
docker push ghcr.io/${{ github.repository }}:$VERSION-cdn
|
||||||
|
docker tag ghcr.io/${{ github.repository }}:$VERSION-cdn ghcr.io/${{ github.repository }}:cdn
|
||||||
|
docker push ghcr.io/${{ github.repository }}:cdn
|
||||||
|
|||||||
31
.github/workflows/eslint.yml
vendored
Normal file
31
.github/workflows/eslint.yml
vendored
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
name: ESLint Lint for Pull Requests
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- '**/*.js'
|
||||||
|
- '**/*.ts'
|
||||||
|
- '**/*.vue'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lint:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
# 检出代码
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
# 设置 Node.js 环境
|
||||||
|
- name: Set up Node.js
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: '20'
|
||||||
|
|
||||||
|
# 安装依赖
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm install
|
||||||
|
|
||||||
|
# 运行 ESLint
|
||||||
|
- name: Run ESLint
|
||||||
|
run: npm run lint
|
||||||
152
.github/workflows/release.yml
vendored
152
.github/workflows/release.yml
vendored
@ -1,6 +1,9 @@
|
|||||||
name: Build and Release
|
name: Build and Release
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*.*.*'
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
version:
|
version:
|
||||||
@ -9,36 +12,42 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-and-release:
|
build-and-release:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-24.04
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
fetch-depth: 20
|
||||||
|
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: '20' # 根据项目需求调整版本
|
node-version: '20'
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: npm install
|
|
||||||
|
|
||||||
- name: Build project
|
|
||||||
run: npm run build
|
|
||||||
|
|
||||||
- name: Get version from package.json
|
- name: Get version from package.json
|
||||||
id: get_version
|
id: get_version
|
||||||
run: echo "::set-output name=version::$(node -p "require('./package.json').version")"
|
run: echo "version=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Determine version
|
- name: Determine version
|
||||||
id: determine_version
|
id: determine_version
|
||||||
run: |
|
run: |
|
||||||
if [ "${{ github.event.inputs.version }}" ]; then
|
if [ "${{ github.event.inputs.version }}" ]; then
|
||||||
echo "::set-output name=version::${{ github.event.inputs.version }}"
|
echo "version=${{ github.event.inputs.version }}" >> $GITHUB_OUTPUT
|
||||||
else
|
else
|
||||||
echo "::set-output name=version::${{ steps.get_version.outputs.version }}"
|
echo "version=${{ steps.get_version.outputs.version }}" >> $GITHUB_OUTPUT
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
- name: Generate release notes
|
||||||
|
id: release_notes
|
||||||
|
run: |
|
||||||
|
echo "#### Changes" > release_notes.md
|
||||||
|
git log -20 --pretty=format:"- %s" >> release_notes.md
|
||||||
|
echo -e "\n-----------\n哪吒V1请下载dist.zip\n哪吒V0请下载v0-dist.zip\n哪吒V0/nazhua/子目录需求请下载v0-nazhua.zip\nv${{ steps.determine_version.outputs.version }}-all.zip是包含字体的全量包\nv${{ steps.determine_version.outputs.version }}-cdn-jsdelivr.zip是jsdelivr引用版\nv${{ steps.determine_version.outputs.version }}-cdn-loli.zip是cdnjs的loli.net引用版" >> release_notes.md
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm install
|
||||||
|
|
||||||
- name: Create Release
|
- name: Create Release
|
||||||
id: create_release
|
id: create_release
|
||||||
uses: actions/create-release@v1
|
uses: actions/create-release@v1
|
||||||
@ -47,19 +56,126 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
tag_name: v${{ steps.determine_version.outputs.version }}
|
tag_name: v${{ steps.determine_version.outputs.version }}
|
||||||
release_name: Release v${{ steps.determine_version.outputs.version }}
|
release_name: Release v${{ steps.determine_version.outputs.version }}
|
||||||
draft: false
|
draft: true
|
||||||
prerelease: false
|
prerelease: false
|
||||||
|
|
||||||
- name: Package build output
|
- name: 构建自动版 - 完整引用版本
|
||||||
run: |
|
run: npm run build
|
||||||
zip -r build-v${{ steps.determine_version.outputs.version }}.zip dist
|
|
||||||
|
|
||||||
- name: Upload Release Asset
|
- name: 打包v${{ steps.determine_version.outputs.version }}-all.zip
|
||||||
|
run: zip -r v${{ steps.determine_version.outputs.version }}-all.zip dist
|
||||||
|
|
||||||
|
- name: 构建自动版 - JSDeliver引用版本
|
||||||
|
env:
|
||||||
|
VITE_SARASA_TERM_SC_USE_CDN: '1'
|
||||||
|
VITE_USE_CDN: '1'
|
||||||
|
VITE_CDN_LIB_TYPE: 'jsdelivr'
|
||||||
|
run: npm run build
|
||||||
|
|
||||||
|
- name: 打包v${{ steps.determine_version.outputs.version }}-cdn-jsdelivr.zip
|
||||||
|
run: zip -r v${{ steps.determine_version.outputs.version }}-cdn-jsdelivr.zip dist
|
||||||
|
|
||||||
|
- name: 构建自动版 - loli(CDNJS)引用版本
|
||||||
|
env:
|
||||||
|
VITE_DISABLE_SARASA_TERM_SC: '1'
|
||||||
|
VITE_USE_CDN: '1'
|
||||||
|
VITE_CDN_LIB_TYPE: 'loli'
|
||||||
|
run: npm run build
|
||||||
|
|
||||||
|
- name: 打包v${{ steps.determine_version.outputs.version }}-cdn-loli.zip
|
||||||
|
run: zip -r v${{ steps.determine_version.outputs.version }}-cdn-loli.zip dist
|
||||||
|
|
||||||
|
- name: 构建哪吒v0子目录版本
|
||||||
|
env:
|
||||||
|
VITE_BASE_PATH: '/nazhua/'
|
||||||
|
VITE_NEZHA_VERSION: 'v0'
|
||||||
|
VITE_DISABLE_SARASA_TERM_SC: '1'
|
||||||
|
run: npm run build
|
||||||
|
|
||||||
|
- name: 打包v0-nazhua.zip
|
||||||
|
run: zip -r v0-nazhua.zip dist
|
||||||
|
|
||||||
|
- name: 构建哪吒v0版本
|
||||||
|
env:
|
||||||
|
VITE_NEZHA_VERSION: 'v0'
|
||||||
|
VITE_DISABLE_SARASA_TERM_SC: '1'
|
||||||
|
run: npm run build
|
||||||
|
|
||||||
|
- name: 打包v0-dist.zip
|
||||||
|
run: zip -r v0-dist.zip dist
|
||||||
|
|
||||||
|
- name: 构建哪吒v1版本
|
||||||
|
env:
|
||||||
|
VITE_NEZHA_VERSION: 'v1'
|
||||||
|
VITE_DISABLE_SARASA_TERM_SC: '1'
|
||||||
|
run: npm run build
|
||||||
|
|
||||||
|
- name: 打包dist.zip
|
||||||
|
run: zip -r dist.zip dist
|
||||||
|
|
||||||
|
- name: Upload v${{ steps.determine_version.outputs.version }}-all.zip
|
||||||
uses: actions/upload-release-asset@v1
|
uses: actions/upload-release-asset@v1
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
with:
|
with:
|
||||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||||
asset_path: ./build-v${{ steps.determine_version.outputs.version }}.zip
|
asset_path: ./v${{ steps.determine_version.outputs.version }}-all.zip
|
||||||
asset_name: build-v${{ steps.determine_version.outputs.version }}.zip
|
asset_name: v${{ steps.determine_version.outputs.version }}-all.zip
|
||||||
asset_content_type: application/zip
|
asset_content_type: application/zip
|
||||||
|
|
||||||
|
- name: Upload v${{ steps.determine_version.outputs.version }}-cdn-jsdelivr.zip
|
||||||
|
uses: actions/upload-release-asset@v1
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
with:
|
||||||
|
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||||
|
asset_path: ./v${{ steps.determine_version.outputs.version }}-cdn-jsdelivr.zip
|
||||||
|
asset_name: v${{ steps.determine_version.outputs.version }}-cdn-jsdelivr.zip
|
||||||
|
asset_content_type: application/zip
|
||||||
|
|
||||||
|
- name: Upload v${{ steps.determine_version.outputs.version }}-cdn-loli.zip
|
||||||
|
uses: actions/upload-release-asset@v1
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
with:
|
||||||
|
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||||
|
asset_path: ./v${{ steps.determine_version.outputs.version }}-cdn-loli.zip
|
||||||
|
asset_name: v${{ steps.determine_version.outputs.version }}-cdn-loli.zip
|
||||||
|
asset_content_type: application/zip
|
||||||
|
|
||||||
|
- name: Upload v0-nazhua.zip
|
||||||
|
uses: actions/upload-release-asset@v1
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
with:
|
||||||
|
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||||
|
asset_path: ./v0-nazhua.zip
|
||||||
|
asset_name: v0-nazhua.zip
|
||||||
|
asset_content_type: application/zip
|
||||||
|
|
||||||
|
- name: Upload v0-dist.zip
|
||||||
|
uses: actions/upload-release-asset@v1
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
with:
|
||||||
|
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||||
|
asset_path: ./v0-dist.zip
|
||||||
|
asset_name: v0-dist.zip
|
||||||
|
asset_content_type: application/zip
|
||||||
|
|
||||||
|
- name: Upload dist.zip
|
||||||
|
uses: actions/upload-release-asset@v1
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
with:
|
||||||
|
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||||
|
asset_path: ./dist.zip
|
||||||
|
asset_name: dist.zip
|
||||||
|
asset_content_type: application/zip
|
||||||
|
|
||||||
|
- name: Add release notes
|
||||||
|
run: |
|
||||||
|
# 更新发布说明
|
||||||
|
gh release edit v${{ steps.determine_version.outputs.version }} --notes-file release_notes.md
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@ -11,6 +11,7 @@ node_modules
|
|||||||
dist
|
dist
|
||||||
dist-ssr
|
dist-ssr
|
||||||
*.local
|
*.local
|
||||||
|
demo
|
||||||
|
|
||||||
# Editor directories and files
|
# Editor directories and files
|
||||||
.vscode/*
|
.vscode/*
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
FROM nginx:1.27.3
|
FROM nginx:1.27-alpine
|
||||||
|
|
||||||
COPY ./dist /home/wwwroot/html
|
COPY ./dist /home/wwwroot/html
|
||||||
COPY ./nginx-default.conf.template /etc/nginx/templates/default.conf.template
|
COPY ./nginx-default.conf.template /etc/nginx/templates/default.conf.template
|
||||||
|
|||||||
199
doc/deploy.md
Normal file
199
doc/deploy.md
Normal file
@ -0,0 +1,199 @@
|
|||||||
|
# 🚀 部署指南
|
||||||
|
|
||||||
|
## 部署概述
|
||||||
|
> Nazhua主题是纯前端项目,可部署在静态服务器上
|
||||||
|
>
|
||||||
|
> **跨域解决注重点**:
|
||||||
|
> - **V0版本**:需解决 `/api/v1/monitor/${id}`、`/ws` 和 `/` 的跨域
|
||||||
|
> - **V1版本**:需解决 `/api/xxx` 和 `/api/v1/ws/server` 的跨域
|
||||||
|
>
|
||||||
|
> 推荐使用 Nginx 或 Caddy 反向代理解决跨域问题
|
||||||
|
|
||||||
|
## 🐳 Docker Compose + Cloudflare Tunnels 部署
|
||||||
|
此方案便于后续更新,只需通过 `docker compose pull` 命令即可更新主题(镜像)。
|
||||||
|
|
||||||
|
### 配置说明
|
||||||
|
- **favicon.ico**:可通过挂载或配置文件指定(默认无)
|
||||||
|
- **config.js**:需单独挂载,建议使用[配置生成器](https://hi2shark.github.io/nazhua-generator/)生成
|
||||||
|
- **style.css**:用于自定义CSS样式,尽量保持选择器稳定
|
||||||
|
|
||||||
|
### 部署示例
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
nazhua:
|
||||||
|
image: ghcr.io/hi2shark/nazhua:latest
|
||||||
|
container_name: nazhua
|
||||||
|
ports:
|
||||||
|
- 80:80
|
||||||
|
# volumes:
|
||||||
|
# - ./favicon.ico:/home/wwwroot/html/favicon.ico:ro # 自定义favicon图标
|
||||||
|
# - ./config.js:/home/wwwroot/html/config.js:ro # 自定义配置文件
|
||||||
|
# - ./style.css:/home/wwwroot/html/style.css:ro # 自定义样式文件
|
||||||
|
environment:
|
||||||
|
- DOMAIN=_ # 监听的域名,默认为_(监听所有)
|
||||||
|
- NEZHA=http://nezha-dashboard.example.com/ # 可以被反向代理nezha主页地址
|
||||||
|
restart: unless-stopped
|
||||||
|
```
|
||||||
|
|
||||||
|
### 💡 小贴士
|
||||||
|
- 推荐使用 docker-compose 部署 Nazhua 与 Nezha Dashboard,并通过 Cloudflare Tunnels 对外提供服务
|
||||||
|
- 如需减少内置库体积,可使用 CDN 版本镜像:`ghcr.io/hi2shark/nazhua:cdn`
|
||||||
|
- 隐藏原面板方案:使用 Zero Trust Tunnels 部署三个容器 (Tunnels、nezha-dashboard、nazhua)
|
||||||
|
- nazhua 通过 docker 内部地址访问 nezha-dashboard
|
||||||
|
- Tunnels 绑定 nazhua 到公开域名
|
||||||
|
- Tunnels 绑定 nezha-dashboard 到需要邮箱/IP验证的私密域名
|
||||||
|
|
||||||
|
## 🌐 自定义Web服务部署
|
||||||
|
|
||||||
|
### 安装步骤
|
||||||
|
1. 在 [Releases页面](https://github.com/hi2shark/nazhua/releases) 下载最新版 `v{Nazhua版本号}-all.zip`
|
||||||
|
2. 解压后将 `dist` 目录文件上传到Web服务目录
|
||||||
|
|
||||||
|
### Nginx配置示例
|
||||||
|
```nginx
|
||||||
|
map $http_upgrade $connection_upgrade {
|
||||||
|
default upgrade;
|
||||||
|
'' close;
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name nazhua.example.com;
|
||||||
|
client_max_body_size 1024m;
|
||||||
|
|
||||||
|
# 哪吒V0的WebSocket服务
|
||||||
|
location /ws {
|
||||||
|
proxy_pass ${NEZHA}ws;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection $connection_upgrade;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
}
|
||||||
|
|
||||||
|
# 哪吒V1的WebSocket服务
|
||||||
|
location /api/v1/ws/server {
|
||||||
|
proxy_pass ${NEZHA}api/v1/ws/server;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection $connection_upgrade;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /api {
|
||||||
|
proxy_pass http://nezha-dashboard.example.com/api;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /nezha/ {
|
||||||
|
proxy_pass http://nezha-dashboard.example.com/;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
root /home/wwwroot/html;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
----
|
||||||
|
**Tips:** V0环境下若想与面板使用同域名,下载 `v0-nazhua.zip` 并将文件上传至面板目录下的 `nazhua` 文件夹
|
||||||
|
|
||||||
|
----
|
||||||
|
|
||||||
|
## ⚙️ 配置文件
|
||||||
|
|
||||||
|
### config.js 配置说明
|
||||||
|
建议使用 [Nazhua 配置生成器](https://hi2shark.github.io/nazhua-generator/) 生成配置文件。
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
window.$$nazhuaConfig = {
|
||||||
|
title: '哪吒监控', // 网站标题
|
||||||
|
footerSlogan: '不要年付!不要年付!不要年付!<span style="color: #f00;">欢迎访问Nazhua探针</span>', // 底部标语,支持html渲染
|
||||||
|
freeAmount: '白嫖', // 免费服务的费用名称
|
||||||
|
infinityCycle: '长期有效', // 无限周期名称
|
||||||
|
buyBtnText: '购买', // 购买按钮文案
|
||||||
|
buyBtnIcon: '', // 购买按钮图标,取自remixicon
|
||||||
|
customBackgroundImage: '', // 自定义的背景图片地址
|
||||||
|
lightBackground: true, // 启用了浅色系背景图,会强制关闭点点背景
|
||||||
|
showFireworks: true, // 是否显示烟花,建议开启浅色系背景
|
||||||
|
showLantern: true, // 是否显示灯笼
|
||||||
|
enableInnerSearch: true, // 启用内部搜索
|
||||||
|
listServerItemTypeToggle: true, // 服务器列表项类型切换
|
||||||
|
listServerItemType: 'row', // 服务器列表项类型 card/row row列表模式移动端自动切换至card
|
||||||
|
listServerStatusType: 'progress', // 服务器状态类型--列表
|
||||||
|
listServerRealTimeShowLoad: true, // 列表显示服务器实时负载
|
||||||
|
detailServerStatusType: 'progress', // 服务器状态类型--详情页
|
||||||
|
simpleColorMode: true, // 服务器状态纯色显示
|
||||||
|
serverStatusLinear: true, // 服务器状态渐变线性显示 - 与pureColorMode互斥
|
||||||
|
disableSarasaTermSC: true, // 禁用Sarasa Term SC字体
|
||||||
|
hideWorldMap: false, // 隐藏地图
|
||||||
|
hideHomeWorldMap: false, // 隐藏首页地图
|
||||||
|
hideDetailWorldMap: false, // 隐藏详情地图
|
||||||
|
homeWorldMapPosition: 'top', // 首页地图位置 top/bottom
|
||||||
|
detailWorldMapPosition: 'top', // 详情页地图位置 top/bottom
|
||||||
|
hideNavbarServerCount: false, // 隐藏服务器数量
|
||||||
|
hideNavbarServerStat: false, // 隐藏服务器统计
|
||||||
|
hideListItemStatusDonut: false, // 隐藏列表项的饼图
|
||||||
|
hideListItemStat: false, // 隐藏列表项的统计信息
|
||||||
|
hideListItemBill: false, // 隐藏列表项的账单信息
|
||||||
|
hideListItemLink: true, // 隐藏列表项的购买链接
|
||||||
|
hideFilter: false, // 隐藏筛选
|
||||||
|
hideTag: false, // 隐藏标签
|
||||||
|
hideDotBG: true, // 隐藏框框里面的点点背景
|
||||||
|
monitorRefreshTime: 10, // 监控刷新时间间隔,单位s(秒), 0为不刷新,为保证不频繁请求源站,最低生效值为10s
|
||||||
|
monitorChartType: 'multi', // 监控图表类型 single/multi
|
||||||
|
monitorChartTypeToggle: true, // 监控图表类型切换
|
||||||
|
filterGPUKeywords: ['Virtual Display'], // 如果GPU名称中包含这些关键字,则过滤掉
|
||||||
|
customCodeMap: {}, // 自定义的地图点信息
|
||||||
|
nezhaVersion: 'v1', // 哪吒版本 不填写则尝试自动识别
|
||||||
|
apiMonitorPath: '/api/v1/monitor/{id}',
|
||||||
|
wsPath: '/ws',
|
||||||
|
nezhaPath: '/nezha/',
|
||||||
|
nezhaV0ConfigType: 'servers', // 哪吒v0数据读取类型
|
||||||
|
v1ApiMonitorPath: '/api/v1/service/{id}',
|
||||||
|
v1WsPath: '/api/v1/ws/server',
|
||||||
|
v1ApiGroupPath: '/api/v1/server-group',
|
||||||
|
v1ApiSettingPath: '/api/v1/setting',
|
||||||
|
v1ApiProfilePath: '/api/v1/profile',
|
||||||
|
v1DashboardUrl: '/dashboard', // v1版本控制台地址
|
||||||
|
v1HideNezhaDashboardBtn: true, // v1版本导航栏控制台入口/登录按钮 在nezhaVersion为v1时有效
|
||||||
|
routeMode: 'h5', // 路由模式
|
||||||
|
customFavicon: '', // 自定义favicon, 填写完整的url地址
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### 🎨 自定义样式
|
||||||
|
通过修改根目录下的 `style.css` 文件实现样式定制:
|
||||||
|
|
||||||
|
```css
|
||||||
|
:root {
|
||||||
|
/* 修改颜色 */
|
||||||
|
/* 地图上标记点的颜色 */
|
||||||
|
--world-map-point-color: #fff;
|
||||||
|
/* 列表项显示的价格颜色 */
|
||||||
|
--list-item-price-color: #ff6;
|
||||||
|
/* 购买链接的主要颜色 */
|
||||||
|
--list-item-buy-link-color: #f00;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 自定义背景图示例 */
|
||||||
|
:root {
|
||||||
|
/* 图片太亮时,增加背景遮罩透明度 */
|
||||||
|
--layout-main-bg-color: rgba(0, 0, 0, 0.75);
|
||||||
|
}
|
||||||
|
.layout-group .layout-bg {
|
||||||
|
/* 添加!important强制背景图替换 */
|
||||||
|
background: url(./bg.jpg) no-repeat 50% 50% !important;
|
||||||
|
background-size: cover;
|
||||||
|
}
|
||||||
|
```
|
||||||
120
doc/public-note.md
Normal file
120
doc/public-note.md
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
# 📝 公开备注配置指南
|
||||||
|
|
||||||
|
[Nazhua配置生成器](https://hi2shark.github.io/nazhua-generator/#/?tab=publicNote)已添加公开备注编辑器,方便大家配置公开备注
|
||||||
|
|
||||||
|
## 🗺️ 点阵地图节点显示
|
||||||
|
|
||||||
|
### 地图说明
|
||||||
|
Nazhua采用的点阵地图是一个并非精准的变形地图,不能使用真实经纬度坐标进行换算定位,因此需要通过自定义坐标来指定位置。
|
||||||
|
|
||||||
|
### 配置方法
|
||||||
|
使用[Nazhua配置生成器](https://hi2shark.github.io/nazhua-generator/)获取内置的点阵地图坐标或者自定义坐标(可以在`config.js`中配置`customCodeMap`添加自定义地图点)
|
||||||
|
在节点的公开备注对象中设置位置代码:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"customData": {
|
||||||
|
"location": "HKG" // 位置代码
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 默认位置映射
|
||||||
|
部分常见地区已有默认映射:
|
||||||
|
- 中国大陆默认显示在北京(v0.4.6后添加)
|
||||||
|
- 美国默认显示在洛杉矶
|
||||||
|
|
||||||
|
## 🔧 customData 字段详解
|
||||||
|
|
||||||
|
### 可用字段
|
||||||
|
| 字段 | 用途 | 版本支持 |
|
||||||
|
|------|------|---------|
|
||||||
|
| `location` | 指定节点地理位置代码 | 全版本 |
|
||||||
|
| `slogan` | 显示节点标语 | 全版本 |
|
||||||
|
| `orderLink` | 购买链接地址 | 全版本 |
|
||||||
|
| `flag` | 自定义国家/地区旗帜 | v0.6.4+ |
|
||||||
|
| `buyBtnText` | 购买按钮文案 | v0.5.3+ |
|
||||||
|
| `buyBtnIcon` | 购买按钮图标 | v0.5.3+ |
|
||||||
|
|
||||||
|
### 示例配置
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"customData": {
|
||||||
|
"location": "HKG",
|
||||||
|
"slogan": "这是一个香港节点",
|
||||||
|
"orderLink": "https://buy.example.com",
|
||||||
|
"buyBtnText": "官网",
|
||||||
|
"buyBtnIcon": "ri-gift-2-line",
|
||||||
|
"flag": "cn"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 💡 链接编码提示
|
||||||
|
由于配置数据无法正常解析符号`&`,请使用URL编码:
|
||||||
|
- 在线工具:[https://www.bejson.com/enc/urlencode/](https://www.bejson.com/enc/urlencode/)
|
||||||
|
- 浏览器控制台:执行`encodeURIComponent('链接内容')`获取编码后内容
|
||||||
|
|
||||||
|
## 📊 原版公开备注支持
|
||||||
|
在哪吒的主题ServerStatus迭代中,nap0o增加了一个公开备注的功能,可以给节点添加额外的展示信息
|
||||||
|
具体字段定义参考 [https://github.com/nezhahq/nezha/pull/425](https://github.com/nezhahq/nezha/pull/425)
|
||||||
|
Nazhua支持原版ServerStatus主题的公开备注字段,支持的字段如下:
|
||||||
|
|
||||||
|
### 账单信息 (billingDataMod)
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"billingDataMod": {
|
||||||
|
"startDate": "2024-10-01T00:00:00+08:00",
|
||||||
|
"endDate": "2024-11-01T00:00:00+08:00",
|
||||||
|
"autoRenewal": "1",
|
||||||
|
"cycle": "月",
|
||||||
|
"amount": "$3.99"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 配置信息 (planDataMod)
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"planDataMod": {
|
||||||
|
"bandwidth": "30Mbps",
|
||||||
|
"trafficVol": "1TB/月",
|
||||||
|
"trafficType": "1",
|
||||||
|
"IPv4": "1",
|
||||||
|
"IPv6": "1",
|
||||||
|
"networkRoute": "CN2,GIA",
|
||||||
|
"extra": "传家宝,AS9929"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔍 完整公开备注示例
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"billingDataMod": {
|
||||||
|
"startDate": "2024-10-01",
|
||||||
|
"endDate": "2024-11-01",
|
||||||
|
"autoRenewal": "1",
|
||||||
|
"cycle": "月",
|
||||||
|
"amount": "$3.99"
|
||||||
|
},
|
||||||
|
"planDataMod": {
|
||||||
|
"bandwidth": "30Mbps",
|
||||||
|
"trafficVol": "1TB/月",
|
||||||
|
"trafficType": "1",
|
||||||
|
"IPv4": "1",
|
||||||
|
"IPv6": "1",
|
||||||
|
"networkRoute": "CN2,GIA",
|
||||||
|
"extra": "传家宝,AS9929"
|
||||||
|
},
|
||||||
|
"customData": {
|
||||||
|
"location": "HKG",
|
||||||
|
"slogan": "这是一个香港节点",
|
||||||
|
"orderLink": "https://buy.example.com",
|
||||||
|
"buyBtnText": "官网",
|
||||||
|
"buyBtnIcon": "ri-gift-2-line",
|
||||||
|
"flag": "cn"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
[Nazhua配置生成器](https://hi2shark.github.io/nazhua-generator/#/?tab=publicNote)已添加公开备注编辑器,方便大家配置公开备注
|
||||||
30
doc/update.md
Normal file
30
doc/update.md
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
# 📝 更新日志
|
||||||
|
|
||||||
|
> 此处仅记录功能性更新,Bug修复不在此记录
|
||||||
|
|
||||||
|
## 📦 v0.6.4 更新
|
||||||
|
- ✨ **新增**: 网络监控折线图拆分单一图表功能
|
||||||
|
- 🌍 **新增**: 公开备注中支持自定义国家/地区旗帜 (`flag` 字段)
|
||||||
|
- 🔄 **新增**: 支持地图在首页与详情页的上下位置切换
|
||||||
|
|
||||||
|
## 📦 v0.5.7 更新
|
||||||
|
- 🖼️ **新增**: 自定义favicon支持
|
||||||
|
|
||||||
|
## 📦 v0.5.4 更新
|
||||||
|
- 🔍 **新增**: 内置搜索功能,支持 `Ctrl+K` 快速打开搜索
|
||||||
|
|
||||||
|
## 📦 v0.5.3 更新
|
||||||
|
- 🛒 **新增**: 支持单独设置服务器购买按钮的文案和图标
|
||||||
|
|
||||||
|
### 使用方法
|
||||||
|
- `buyBtnText`: 设置购买按钮文案
|
||||||
|
- `buyBtnIcon`: 设置购买按钮图标,支持Remixicon图标
|
||||||
|
|
||||||
|
### 图标配置示例
|
||||||
|
1. 访问 [Remixicon官网](https://www.remixicon.com/)
|
||||||
|
2. 选择并复制图标名称
|
||||||
|
3. 在 `buyBtnIcon` 字段中填写,补齐 `ri-` 前缀
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
> 当前支持版本: Remixicon 4.6.0(cdn版本,受限于更新原因,支持到4.3.0)
|
||||||
6
fonts/SarasaTermSC/font.css
Normal file
6
fonts/SarasaTermSC/font.css
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
@font-face {
|
||||||
|
font-family: "Sarasa Term SC";
|
||||||
|
src: url("./SarasaTermSC-SemiBold.woff2") format("woff2"),
|
||||||
|
url("./SarasaTermSC-SemiBold.woff") format("woff");
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
19
fonts/readme.md
Normal file
19
fonts/readme.md
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
# Nazhua内置字体
|
||||||
|
|
||||||
|
## Sarasa Term SC
|
||||||
|
字体出处:[Sarasa-Gothic](https://github.com/be5invis/Sarasa-Gothic)
|
||||||
|
具体引用:`Sarasa Term SC SemiBold`
|
||||||
|
由TTF转换为WOFF2格式,以便在网页中使用。
|
||||||
|
使用方法:
|
||||||
|
```css
|
||||||
|
@font-face {
|
||||||
|
font-family: "Sarasa Term SC";
|
||||||
|
src: url("./fonts/SarasaTermSC/SarasaTermSC-SemiBold.woff2") format("woff2"),
|
||||||
|
url("./fonts/SarasaTermSC/SarasaTermSC-SemiBold.woff") format("woff");
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sarasa-term-sc {
|
||||||
|
font-family: "Sarasa Term SC";
|
||||||
|
}
|
||||||
|
```
|
||||||
@ -1,3 +1,8 @@
|
|||||||
|
map $http_upgrade $connection_upgrade {
|
||||||
|
default upgrade;
|
||||||
|
'' close;
|
||||||
|
}
|
||||||
|
|
||||||
server {
|
server {
|
||||||
listen 80;
|
listen 80;
|
||||||
server_name ${DOMAIN};
|
server_name ${DOMAIN};
|
||||||
@ -7,7 +12,18 @@ server {
|
|||||||
proxy_pass ${NEZHA}ws;
|
proxy_pass ${NEZHA}ws;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
proxy_set_header Connection "upgrade";
|
proxy_set_header Connection $connection_upgrade;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
}
|
||||||
|
|
||||||
|
# 兼容哪吒V1
|
||||||
|
location /api/v1/ws/server {
|
||||||
|
proxy_pass ${NEZHA}api/v1/ws/server;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection $connection_upgrade;
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
|||||||
5187
package-lock.json
generated
Normal file
5187
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
29
package.json
29
package.json
@ -1,38 +1,45 @@
|
|||||||
{
|
{
|
||||||
"name": "nazhua",
|
"name": "nazhua",
|
||||||
"version": "0.3.8",
|
"version": "0.9.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"preview": "vite preview"
|
"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",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"lint": "eslint .",
|
||||||
|
"lint:fix": "eslint . --fix"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.7.7",
|
"axios": "^1.13.2",
|
||||||
"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.5.0",
|
"remixicon": "^4.7.0",
|
||||||
|
"uniqolor": "^1.1.1",
|
||||||
"vue": "^3.5.12",
|
"vue": "^3.5.12",
|
||||||
"vue-echarts": "^7.0.3",
|
"vue-echarts": "^7.0.3",
|
||||||
"vue-router": "^4.4.5",
|
"vue-router": "^4.4.5",
|
||||||
"vuex": "^4.1.0"
|
"vuex": "^4.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.24.9",
|
"@babel/core": "^7.28.5",
|
||||||
"@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.1.4",
|
"@vitejs/plugin-vue": "^5.2.4",
|
||||||
"@vue/eslint-config-airbnb": "^7.0.0",
|
"@vue/eslint-config-airbnb": "^7.0.0",
|
||||||
|
"cross-env": "^7.0.3",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"eslint": "^8.34.0",
|
"eslint": "^8.57.1",
|
||||||
"eslint-plugin-vue": "^9.9.0",
|
"eslint-plugin-vue": "^9.33.0",
|
||||||
"sass": "^1.81.0",
|
"sass": "^1.81.0",
|
||||||
"vite": "^5.4.10",
|
"vite": "^6.4.1",
|
||||||
"vite-plugin-babel": "^1.2.0",
|
"vite-plugin-babel": "^1.3.2",
|
||||||
"vite-plugin-eslint": "^1.8.1"
|
"vite-plugin-eslint": "^1.8.1",
|
||||||
|
"vite-svg-loader": "^5.1.0"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|||||||
@ -1,24 +1,56 @@
|
|||||||
window.$$nazhuaConfig = {
|
window.$$nazhuaConfig = {
|
||||||
// title: '哪吒监控', // 网站标题
|
// title: '哪吒监控', // 网站标题
|
||||||
|
// footerSlogan: '不要年付!不要年付!不要年付!<span style="color: #f00;">欢迎访问Nazhua探针</span>',
|
||||||
// freeAmount: '白嫖', // 免费服务的费用名称
|
// freeAmount: '白嫖', // 免费服务的费用名称
|
||||||
// infinityCycle: '无限', // 无限周期名称
|
// infinityCycle: '长期有效', // 无限周期名称
|
||||||
// buyBtnText: '购买', // 购买按钮文案
|
// buyBtnText: '购买', // 购买按钮文案
|
||||||
|
// buyBtnIcon: '', // 购买按钮图标,取自remixicon
|
||||||
|
// customBackgroundImage: '', // 自定义的背景图片地址
|
||||||
|
// lightBackground: true, // 启用了浅色系背景图,会强制关闭点点背景
|
||||||
|
// showFireworks: true, // 是否显示烟花,建议开启浅色系背景
|
||||||
|
// showLantern: true, // 是否显示灯笼
|
||||||
|
enableInnerSearch: true, // 启用内部搜索
|
||||||
|
// listServerItemTypeToggle: true, // 服务器列表项类型切换
|
||||||
|
listServerItemType: 'card', // 服务器列表项类型 card/row/server-status row列表模式移动端自动切换至card
|
||||||
|
// serverStatusColumnsTpl: null, // 服务器状态列配置模板
|
||||||
// listServerStatusType: 'progress', // 服务器状态类型--列表
|
// listServerStatusType: 'progress', // 服务器状态类型--列表
|
||||||
|
// listServerRealTimeShowLoad: true, // 列表显示服务器实时负载
|
||||||
// detailServerStatusType: 'progress', // 服务器状态类型--详情页
|
// detailServerStatusType: 'progress', // 服务器状态类型--详情页
|
||||||
// hideNavbarServerCount: false, // 隐藏服务器数量
|
// simpleColorMode: true, // 服务器状态纯色显示
|
||||||
// hideNavbarServerStat: false, // 隐藏服务器统计
|
serverStatusLinear: true, // 服务器状态渐变线性显示 - 与pureColorMode互斥
|
||||||
// hideListItemStatusDonut: true, // 隐藏列表项的饼图
|
// disableSarasaTermSC: true, // 禁用Sarasa Term SC字体
|
||||||
// hideListItemStat: true, // 隐藏列表项的统计信息
|
|
||||||
// hideListItemBill: false, // 隐藏列表项的账单信息
|
|
||||||
// hideWorldMap: false, // 隐藏地图
|
// hideWorldMap: false, // 隐藏地图
|
||||||
// hideHomeWorldMap: false, // 隐藏首页地图
|
// hideHomeWorldMap: false, // 隐藏首页地图
|
||||||
// hideDetailWorldMap: false, // 隐藏详情地图
|
// hideDetailWorldMap: false, // 隐藏详情地图
|
||||||
|
// homeWorldMapPosition: 'top', // 首页地图位置 top/bottom
|
||||||
|
// detailWorldMapPosition: 'top', // 详情页地图位置 top/bottom
|
||||||
|
// hideNavbarServerCount: false, // 隐藏服务器数量
|
||||||
|
// hideNavbarServerStat: false, // 隐藏服务器统计
|
||||||
|
// hideListItemStatusDonut: false, // 隐藏列表项的饼图
|
||||||
|
// hideListItemStat: false, // 隐藏列表项的统计信息
|
||||||
|
// hideListItemBill: false, // 隐藏列表项的账单信息
|
||||||
|
hideListItemLink: true, // 隐藏列表项的购买链接
|
||||||
// hideFilter: false, // 隐藏筛选
|
// hideFilter: false, // 隐藏筛选
|
||||||
|
// hideSort: false, // 隐藏排序
|
||||||
// hideTag: false, // 隐藏标签
|
// hideTag: false, // 隐藏标签
|
||||||
|
// hideDotBG: true, // 隐藏框框里面的点点背景
|
||||||
|
// monitorRefreshTime: 10, // 监控刷新时间间隔,单位s(秒), 0为不刷新,为保证不频繁请求源站,最低生效值为10s
|
||||||
|
monitorChartType: 'multi', // 监控图表类型 single/multi
|
||||||
|
monitorChartTypeToggle: true, // 监控图表类型切换
|
||||||
|
// filterGPUKeywords: ['Virtual Display'], // 如果GPU名称中包含这些关键字,则过滤掉
|
||||||
// customCodeMap: {}, // 自定义的地图点信息
|
// customCodeMap: {}, // 自定义的地图点信息
|
||||||
|
// nezhaVersion: 'v1', // 哪吒版本
|
||||||
// apiMonitorPath: '/api/v1/monitor/{id}',
|
// apiMonitorPath: '/api/v1/monitor/{id}',
|
||||||
// wsPath: '/ws',
|
// wsPath: '/ws',
|
||||||
// nezhaPath: '/nezha/',
|
// nezhaPath: '/nezha/',
|
||||||
// nezhaV0ConfigType: 'servers', // 哪吒v0数据读取类型
|
// nezhaV0ConfigType: 'servers', // 哪吒v0数据读取类型
|
||||||
|
// v1ApiMonitorPath: '/api/v1/service/{id}',
|
||||||
|
// v1WsPath: '/api/v1/ws/server',
|
||||||
|
// v1ApiGroupPath: '/api/v1/server-group',
|
||||||
|
// v1ApiSettingPath: '/api/v1/setting',
|
||||||
|
// v1ApiProfilePath: '/api/v1/profile',
|
||||||
|
// v1DashboardUrl: '/dashboard', // v1版本控制台地址
|
||||||
|
// v1HideNezhaDashboardBtn: true, // v1版本导航栏控制台入口/登录按钮 在nezhaVersion为v1时有效
|
||||||
// routeMode: 'h5', // 路由模式
|
// routeMode: 'h5', // 路由模式
|
||||||
|
// customFavicon: '', // 自定义favicon, 填写完整的url地址
|
||||||
};
|
};
|
||||||
|
|||||||
230
readme.md
230
readme.md
@ -1,180 +1,88 @@
|
|||||||
# Nazhua
|
# Nazhua
|
||||||
基于哪吒监控(nezha.wiki)v0版本构建的前端主题,目前暂不支持v1版本,关于v1支持需要等待后续版本。
|
|
||||||
主题有点**重**,因为内置了一个带中文的`SarasaTermSC-SemiBold`字体。
|
|
||||||
|
|
||||||
## 劝退指南 用前必读
|
<div>
|
||||||
1. 本主题是基于哪吒监控v0版本构建的,不支持v1版本。*未来根据情况可能会支持v1版本*
|
<img src="./.github/images/nazhua-main.webp" style="max-height: 500px;" alt="Nazhua桌面版"/>
|
||||||
2. 本主题是一个纯前端项目,需要解决跨域问题,通常需要一个nginx或者caddy反代请求解决跨域问题。
|
<img src="./.github/images/nazhua-mobile.webp" style="max-height: 500px;" alt="Nazhua移动版"/>
|
||||||
3. 我不会提供任何技术支持,如果你有问题,可以提issue,但是我不保证会回答,可能询问GPT会更快。
|
<img src="./.github/images/nazhua-detail-mobile.webp" style="max-height: 500px;" alt="Nazhua详情页"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
## 关于点阵地图
|
## 📢 使用须知
|
||||||
点阵地图是一个失真的地图,地图边际与城市位置都不是真实的经纬度坐标,因此无法通过经纬度来定位城市。
|
|
||||||
需要在是[Nazhua配置生成器](https://hi2shark.github.io/nazhua-generator/)中,拾取点阵地图上的坐标,然后在`config.js`中配置`customCodeMap`来自定义地图点信息。
|
|
||||||
如何指定节点的地理位置?
|
|
||||||
在哪吒监控后台,给节点的公开备注对象中,添加一个`customData`对象,并指定`location`的代码;
|
|
||||||
关于都有哪些内置的地理位置代码,需要在[Nazhua配置生成器](https://hi2shark.github.io/nazhua-generator/)中查看。
|
|
||||||
示例
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"customData": {
|
|
||||||
"location": "HKG"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
对于几个我常见的国别位置,添加了默认映射位置,会自动显示在地图上。(美国太大了,就默认显示在最常买的位置:洛杉矶)
|
|
||||||
|
|
||||||
## 关于节点slogan和购买链接
|
**使用前,请务必阅读本文档,对您的部署会有很大帮助**
|
||||||
同时,这个`customData`中还可以添加一项`slogan`和`orderLink`字符串,分别用于显示节点的标语和购买链接。
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"customData": {
|
|
||||||
"location": "HKG",
|
|
||||||
"slogan": "这是一个香港节点",
|
|
||||||
"orderLink": "https://buy.example.com"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
Tips:
|
|
||||||
由于配置数据获取的方式特殊,无法正常解析符号`&`,建议在[https://www.bejson.com/enc/urlencode/](https://www.bejson.com/enc/urlencode/)进行编码后,将encodeURIComponent编码内容添加到orderLink中。
|
|
||||||
当然,你也可以通过浏览器的console(控制台),执行`encodeURIComponent('链接内容')`,获取编码后的内容。
|
|
||||||
|
|
||||||
## 对于公开备注的支持
|
- 基于哪吒监控(nezha.wiki)v0版本构建的前端主题,兼容v1版本数据结构
|
||||||
在哪吒的主题ServerStatus迭代中,nap0o增加了一个公开备注的功能,可以给节点添加额外的展示信息
|
- 考虑到国内用户访问需求,默认使用cdnjs的loli.net作为CDN引用源
|
||||||
具体字段定义参考 [https://github.com/nezhahq/nezha/pull/425](https://github.com/nezhahq/nezha/pull/425)
|
- 如需使用SarasaTermSC字体,请选择Docker镜像全量包进行部署
|
||||||
Nazhua对这个支持大概在90%左右,参与数据处理了的字段如下:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"billingDataMod": {
|
|
||||||
"startDate": "2024-10-01T00:00:00+08:00",
|
|
||||||
"endDate": "2024-11-01T00:00:00+08:00",
|
|
||||||
"autoRenewal": "1",
|
|
||||||
"cycle": "月",
|
|
||||||
"amount": "$3.99"
|
|
||||||
},
|
|
||||||
"planDataMod": {
|
|
||||||
"bandwidth": "30Mbps",
|
|
||||||
"trafficVol": "1TB/月",
|
|
||||||
"trafficType": "1",
|
|
||||||
"networkRoute": "CN2,GIA",
|
|
||||||
"extra": "传家宝,AS9929"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
其中IPv4、IPv6暂未参与到处理中,后续可能会支持。
|
|
||||||
|
|
||||||
## 数据来源
|
## 🚀 部署指南
|
||||||
1. 公开的全量配置,其中包括“公开备注”(PublicNote),来着探针主页上暴露的服务器节点列表配置信息。此处是根据正则匹配的方式,获取到的节点列表。在主题项目中,默认将访问`/nezha/`的指向此处。
|
|
||||||
2. 实时数据,来着公开的ws服务接口,`/ws`。
|
|
||||||
3. 监控数据,来着公开的api接口,`/api/v1/monitor/${id}`。
|
|
||||||
|
|
||||||
## 部署
|
**推荐使用Docker Compose + Cloudflare Tunnels部署Nazhua**
|
||||||
Nazhua主题是一个纯前端项目,可以部署在纯静态服务器上,但需要解决`/api/v1/monitor/${id}`监控数据、`/ws`WS服务和`/`主页的跨域访问。
|
|
||||||
通常来说,你需要一个nginx或者caddy反代请求解决跨域问题。
|
|
||||||
|
|
||||||
### Docker Compose + Cloudflare Tunnels部署
|
👉 [详细部署文档](./doc/deploy.md)
|
||||||
**请关注备注中的提示内容**
|
|
||||||
```yaml
|
|
||||||
services:
|
|
||||||
nazhua:
|
|
||||||
image: ghcr.io/hi2shark/nazhua:latest
|
|
||||||
container_name: nazhua
|
|
||||||
ports:
|
|
||||||
- 80:80
|
|
||||||
volumes:
|
|
||||||
# - ./favicon.ico:/home/wwwroot/html/favicon.ico:ro # 自定义favicon图标
|
|
||||||
# - ./config.js:/home/wwwroot/html/config.js:ro # 自定义配置文件
|
|
||||||
# - ./style.css:/home/wwwroot/html/style.css:ro # 自定义样式文件
|
|
||||||
environment:
|
|
||||||
- DOMAIN=_ # 监听的域名,默认为_(监听所有)
|
|
||||||
- NEZHA=http://nezha-dashboard.example.com/ # 可以被反代nezha主页地址
|
|
||||||
restart: unless-stopped
|
|
||||||
```
|
|
||||||
建议通过docker-compose部署服务,然后通过cloudflare的tunnels向外提供服务,可以不用自己配置https证书。
|
|
||||||
|
|
||||||
### Nginx配置示例
|
Nazhua提供了丰富的配置选项:
|
||||||
```nginx
|
- 支持点阵地图显示/隐藏
|
||||||
server {
|
- 首页风格切换等多种个性化设置
|
||||||
listen 80;
|
|
||||||
server_name nazhua.example.com;
|
|
||||||
|
|
||||||
location /ws {
|
配置方式:
|
||||||
proxy_pass http://nezha-dashboard.example.com/ws;
|
- **V1内置版本**:使用[配置生成器](https://hi2shark.github.io/nazhua-generator/)生成配置,填入控制台自定义代码
|
||||||
proxy_http_version 1.1;
|
- **Docker部署**:手动配置`config.js`文件(包括v0版本)
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
|
||||||
proxy_set_header Connection "upgrade";
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
}
|
|
||||||
|
|
||||||
location /api {
|
## 🗺️ 节点位置配置
|
||||||
proxy_pass http://nezha-dashboard.example.com/api;
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
}
|
|
||||||
|
|
||||||
location /nezha/ {
|
要在地图上显示节点位置,需在公开备注中指定`location`字段
|
||||||
proxy_pass http://nezha-dashboard.example.com/;
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
}
|
|
||||||
|
|
||||||
location / {
|
👉 [公开备注配置文档](./doc/public-note.md)
|
||||||
try_files $uri $uri/ /index.html;
|
|
||||||
root /home/wwwroot/html;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
|
## 📝 更新日志
|
||||||
|
|
||||||
## 自定义配置
|
👉 [功能更新记录](./doc/update.md)
|
||||||
可以通过修改根目录下的`config.js`文件来自定义配置
|
|
||||||
例如:(*参考内容在文档上不一定是最新,具体参考public/config.js或者[Nazhua配置生成器](https://hi2shark.github.io/nazhua-generator/)*)
|
|
||||||
```javascript
|
|
||||||
window.$$nazhuaConfig = {
|
|
||||||
title: '哪吒监控', // 网站标题
|
|
||||||
freeAmount: '', // 免费服务的费用名称
|
|
||||||
infinityCycle: '', // 无限周期名称
|
|
||||||
hideListItemBill: false, // 隐藏列表项的账单信息
|
|
||||||
buyBtnText: '', // 购买按钮文案
|
|
||||||
hideWorldMap: false, // 隐藏地图
|
|
||||||
hideHomeWorldMap: false, // 隐藏首页地图
|
|
||||||
hideDetailWorldMap: false, // 隐藏详情地图
|
|
||||||
hideFilter: false, // 隐藏筛选
|
|
||||||
hideTag: false, // 隐藏标签
|
|
||||||
customCodeMap: {}, // 自定义的地图点信息
|
|
||||||
apiMonitorPath: '/api/v1/monitor/{id}',
|
|
||||||
wsPath: '/ws',
|
|
||||||
nezhaPath: '/nezha/',
|
|
||||||
nezhaV0ConfigType: 'servers',
|
|
||||||
routeMode: 'h5', // 路由模式 h5 | hash
|
|
||||||
};
|
|
||||||
```
|
|
||||||
可以通过修改根目录下的`style.css`文件来自定义样式
|
|
||||||
例如:
|
|
||||||
```css
|
|
||||||
:root {
|
|
||||||
/* 修改颜色 */
|
|
||||||
/* 地图上标记点的颜色 */
|
|
||||||
--world-map-point-color: #fff;
|
|
||||||
/* 列表项显示的价格颜色 */
|
|
||||||
--list-item-price-color: #ff6;
|
|
||||||
/* 购买链接的主要颜色 */
|
|
||||||
--list-item-buy-link-color: #f00;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
可以通过[Nazhua配置生成器](https://hi2shark.github.io/nazhua-generator/)快速生成config.js配置文件
|
|
||||||
|
|
||||||
|
## 🤝 赞助商
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<td align="center">
|
||||||
|
<a href="https://www.vmiss.com" target="_blank" title="VMISS,加拿大企业,打造全球优质优化线路。提供香港、日本、韩国、美国、英国的云服务器">
|
||||||
|
<img src="./.github/images/vmiss-logo.jpg" width="200px;" alt="VMISS"/>
|
||||||
|
</a>
|
||||||
|
<br />
|
||||||
|
<strong>VMISS</strong>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
## 💻 开发者指南
|
||||||
|
|
||||||
|
### 环境配置
|
||||||
|
|
||||||
|
在`.env.development.local`中配置以下变量:
|
||||||
|
|
||||||
## 二次开发提示
|
|
||||||
`.env.development.local`配置变量
|
|
||||||
```bash
|
```bash
|
||||||
WS_HOST=http://127.0.0.1:9288 # 本地nezha ws反代
|
#### Sarasa Term SC字体设置
|
||||||
API_HOST=http://nezha-dashboard.example.com # 本地nezha api反代
|
# VITE_DISABLE_SARASA_TERM_SC=1
|
||||||
NEZHA_HOST=http://nezha-dashboard.example.com # 本地nezha主页反代
|
# VITE_SARASA_TERM_SC_USE_CDN=1
|
||||||
NEZHA_HOST_REPACE_PATH=1 # 是否替换主页路径`/nezha/`
|
|
||||||
|
#### CDN配置
|
||||||
|
# VITE_USE_CDN=1
|
||||||
|
# VITE_CDN_LIB_TYPE=jsdelivr # jsdelivr | cdnjs | loli
|
||||||
|
|
||||||
|
#### 哪吒版本控制
|
||||||
|
# VITE_NEZHA_VERSION=v1 # v0 | v1
|
||||||
|
|
||||||
|
#### 本地开发设置
|
||||||
|
# PROXY_WS_HOST= # 本地开发时,可以代理WS服务的地址,启用后,自动转发至 {PROXY_WS_HOST}/proxy?wsPath={WS_HOST}
|
||||||
|
# API_HOST= # 本地开发时,代理的API服务地址
|
||||||
|
# WS_HOST= # 本地开发时,代理的WS服务地址
|
||||||
|
##### 仅限v0版本
|
||||||
|
# NEZHA_HOST= # 本地开发时,代理的哪吒主页地址
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 数据来源参考
|
||||||
|
|
||||||
|
| 数据类型 | V0版本 | V1版本 |
|
||||||
|
|---------|--------|--------|
|
||||||
|
| 全量配置 | 公开备注(PublicNote):通过正则匹配节点列表,默认访问`/nezha/` | - |
|
||||||
|
| 实时数据 | WS接口:`/ws` | WS接口:`/api/v1/ws/server` |
|
||||||
|
| 监控数据 | API接口:`/api/v1/monitor/${id}` | API接口:`/api/v1/service/${id}` |
|
||||||
|
| 分组数据 | 服务器节点列表的`Tag`字段匹配 | API接口:`/api/v1/server-group` |
|
||||||
|
|||||||
83
src/App.vue
83
src/App.vue
@ -1,19 +1,31 @@
|
|||||||
<template>
|
<template>
|
||||||
<layout-main>
|
<layout-main>
|
||||||
<router-view />
|
<router-view v-slot="{ Component }">
|
||||||
|
<keep-alive>
|
||||||
|
<component :is="Component" />
|
||||||
|
</keep-alive>
|
||||||
|
</router-view>
|
||||||
</layout-main>
|
</layout-main>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import {
|
import {
|
||||||
ref,
|
ref,
|
||||||
|
computed,
|
||||||
|
watch,
|
||||||
provide,
|
provide,
|
||||||
onMounted,
|
onMounted,
|
||||||
|
onUnmounted,
|
||||||
} from 'vue';
|
} from 'vue';
|
||||||
import { useStore } from 'vuex';
|
import { useStore } from 'vuex';
|
||||||
|
import { useRoute } from 'vue-router';
|
||||||
|
import config, {
|
||||||
|
init as initConfig,
|
||||||
|
} from '@/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,
|
||||||
@ -21,25 +33,48 @@ import activeWebsocketService, {
|
|||||||
} from './ws';
|
} from './ws';
|
||||||
|
|
||||||
const store = useStore();
|
const store = useStore();
|
||||||
|
const route = useRoute();
|
||||||
|
|
||||||
const currentTime = ref(0);
|
const currentTime = ref(0);
|
||||||
|
|
||||||
provide('currentTime', currentTime);
|
provide('currentTime', currentTime);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 刷新当前时间
|
||||||
|
* 使用 requestAnimationFrame 持续更新时间,但只在秒级变化时更新值以减少不必要的响应式更新
|
||||||
|
*/
|
||||||
|
let lastUpdateTime = 0;
|
||||||
function refreshTime() {
|
function refreshTime() {
|
||||||
currentTime.value = Date.now();
|
const now = Date.now();
|
||||||
setTimeout(() => {
|
// 只在秒级变化时更新,减少响应式更新频率
|
||||||
refreshTime();
|
if (Math.floor(now / 1000) !== Math.floor(lastUpdateTime / 1000)) {
|
||||||
}, 1000);
|
currentTime.value = now;
|
||||||
|
lastUpdateTime = now;
|
||||||
}
|
}
|
||||||
|
window.requestAnimationFrame(refreshTime);
|
||||||
|
}
|
||||||
|
refreshTime();
|
||||||
|
|
||||||
function handleSystem() {
|
// 是否为Windows系统
|
||||||
const isWindows = /windows|win32/i.test(navigator.userAgent);
|
const isWindows = /windows|win32/i.test(navigator.userAgent);
|
||||||
if (isWindows) {
|
if (isWindows) {
|
||||||
document.body.classList.add('windows');
|
document.body.classList.add('windows');
|
||||||
}
|
}
|
||||||
|
// 是否加载Sarasa Term SC字体
|
||||||
|
const loadSarasaTermSC = computed(() => config.nazhua.disableSarasaTermSC !== true);
|
||||||
|
watch(loadSarasaTermSC, (value) => {
|
||||||
|
if (value) {
|
||||||
|
document.body.classList.add('sarasa-term-sc');
|
||||||
|
} else {
|
||||||
|
document.body.classList.remove('sarasa-term-sc');
|
||||||
}
|
}
|
||||||
|
}, {
|
||||||
|
immediate: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* websocket断连的自动重连
|
||||||
|
*/
|
||||||
let stopReconnect = false;
|
let stopReconnect = false;
|
||||||
async function wsReconnect() {
|
async function wsReconnect() {
|
||||||
if (stopReconnect) {
|
if (stopReconnect) {
|
||||||
@ -53,9 +88,23 @@ async function wsReconnect() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
handleSystem();
|
|
||||||
refreshTime();
|
refreshTime();
|
||||||
await store.dispatch('loadServers');
|
|
||||||
|
// 如果没有配置哪吒版本,尝试载入 v1 版本配置
|
||||||
|
if (!config.init) {
|
||||||
|
await initConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化服务器信息
|
||||||
|
*/
|
||||||
|
await store.dispatch('initServerInfo', {
|
||||||
|
route,
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化WS重连维护
|
||||||
|
*/
|
||||||
msg.on('close', () => {
|
msg.on('close', () => {
|
||||||
console.log('ws closed');
|
console.log('ws closed');
|
||||||
wsReconnect();
|
wsReconnect();
|
||||||
@ -68,13 +117,21 @@ onMounted(async () => {
|
|||||||
console.log('ws connected');
|
console.log('ws connected');
|
||||||
store.dispatch('watchWsMsg');
|
store.dispatch('watchWsMsg');
|
||||||
});
|
});
|
||||||
activeWebsocketService();
|
const handleFocus = () => {
|
||||||
|
// ws在离开焦点后出现断连,尝试重新连接
|
||||||
// 监听窗口重新获得焦点
|
// 仅针对已关闭状态进行重连
|
||||||
window.addEventListener('focus', () => {
|
if (wsService.connected === WS_CONNECTION_STATUS.CLOSED) {
|
||||||
if (wsService.connected !== 1) {
|
|
||||||
restart();
|
restart();
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener('focus', handleFocus);
|
||||||
|
/**
|
||||||
|
* 激活websocket服务
|
||||||
|
*/
|
||||||
|
activeWebsocketService();
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
window.removeEventListener('focus', handleFocus);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
BIN
src/assets/fonts/SarasaTermSC/SarasaTermSC-SemiBold.woff
Normal file
BIN
src/assets/fonts/SarasaTermSC/SarasaTermSC-SemiBold.woff
Normal file
Binary file not shown.
BIN
src/assets/fonts/SarasaTermSC/SarasaTermSC-SemiBold.woff2
Normal file
BIN
src/assets/fonts/SarasaTermSC/SarasaTermSC-SemiBold.woff2
Normal file
Binary file not shown.
6
src/assets/fonts/SarasaTermSC/cdn-font.css
Normal file
6
src/assets/fonts/SarasaTermSC/cdn-font.css
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
@font-face {
|
||||||
|
font-family: "Sarasa Term SC";
|
||||||
|
src: url("https://cdn.jsdelivr.net/gh/hi2shark/nazhua@main/fonts/SarasaTermSC/SarasaTermSC-SemiBold.woff2") format("woff2"),
|
||||||
|
url("https://cdn.jsdelivr.net/gh/hi2shark/nazhua@main/fonts/SarasaTermSC/SarasaTermSC-SemiBold.woff") format("woff");
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
6
src/assets/fonts/SarasaTermSC/font.css
Normal file
6
src/assets/fonts/SarasaTermSC/font.css
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
@font-face {
|
||||||
|
font-family: "Sarasa Term SC";
|
||||||
|
src: url("./SarasaTermSC-SemiBold.woff2") format("woff2"),
|
||||||
|
url("./SarasaTermSC-SemiBold.woff") format("woff");
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
@ -4,8 +4,8 @@ body {
|
|||||||
line-height: 1.8;
|
line-height: 1.8;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-family: 'Microsoft YaHei', '微软雅黑', 'PingFang SC', 'HanHei SC', 'Helvetica Neue', 'Helvetica', 'STHeitiSC-Light', 'Arial', sans-serif;
|
font-family: 'Microsoft YaHei', '微软雅黑', 'PingFang SC', 'HanHei SC', 'Helvetica Neue', 'Helvetica', 'STHeitiSC-Light', 'Arial', sans-serif;
|
||||||
color: #555;
|
color: var(--global-text-color);
|
||||||
background: #fff;
|
background: var(--global-background-color);
|
||||||
-webkit-text-size-adjust: none;
|
-webkit-text-size-adjust: none;
|
||||||
text-size-adjust: none;
|
text-size-adjust: none;
|
||||||
}
|
}
|
||||||
@ -97,7 +97,7 @@ a:hover {
|
|||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: #369;
|
color: var(--global-link-color);
|
||||||
transition: color 150ms linear;
|
transition: color 150ms linear;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
@ -141,24 +141,3 @@ h6 {
|
|||||||
div:focus {
|
div:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: "Sarasa Term SC";
|
|
||||||
src: url("../fonts/SarasaTermSC-SemiBold.woff2") format("woff2"),
|
|
||||||
url("../fonts/SarasaTermSC-SemiBold.woff2") format("woff");
|
|
||||||
font-display: swap;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-family:
|
|
||||||
"Sarasa Term SC",
|
|
||||||
'Microsoft YaHei',
|
|
||||||
'微软雅黑',
|
|
||||||
'PingFang SC',
|
|
||||||
'HanHei SC',
|
|
||||||
'Helvetica Neue',
|
|
||||||
'Helvetica',
|
|
||||||
'STHeitiSC-Light',
|
|
||||||
'Arial',
|
|
||||||
sans-serif;
|
|
||||||
}
|
|
||||||
|
|||||||
13
src/assets/scss/sarasa-term-sc.scss
Normal file
13
src/assets/scss/sarasa-term-sc.scss
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
body.sarasa-term-sc {
|
||||||
|
font-family:
|
||||||
|
"Sarasa Term SC",
|
||||||
|
'Microsoft YaHei',
|
||||||
|
'微软雅黑',
|
||||||
|
'PingFang SC',
|
||||||
|
'HanHei SC',
|
||||||
|
'Helvetica Neue',
|
||||||
|
'Helvetica',
|
||||||
|
'STHeitiSC-Light',
|
||||||
|
'Arial',
|
||||||
|
sans-serif;
|
||||||
|
}
|
||||||
@ -7,22 +7,44 @@
|
|||||||
|
|
||||||
--detail-container-width: 900px;
|
--detail-container-width: 900px;
|
||||||
|
|
||||||
|
--global-background-color: #392f41;
|
||||||
|
--global-text-color: #ddd;
|
||||||
|
--global-link-color: #2ca9e1;
|
||||||
|
|
||||||
--layout-main-bg-color: rgba(20, 30, 40, 0.75);
|
--layout-main-bg-color: rgba(20, 30, 40, 0.75);
|
||||||
--layout-bg-color: #252748;
|
--layout-bg-color: #252748;
|
||||||
|
|
||||||
--world-map-point-color: #fff143;
|
--world-map-point-color: #fff143;
|
||||||
|
|
||||||
--duration-color: #cbf1f5;
|
--duration-color: #89c3eb;
|
||||||
--transfer-color: #f9ed69;
|
--transfer-color: #f9ed69;
|
||||||
--transfer-in-color: var(--transfer-color);
|
--transfer-in-color: var(--transfer-color);
|
||||||
--transfer-out-color: #90f2ff;
|
--transfer-out-color: #90f2ff;
|
||||||
|
--net-speed-color: #90f2ff;
|
||||||
--net-speed-in-color: #f5b199;
|
--net-speed-in-color: #f5b199;
|
||||||
--net-speed-out-color: #89c3eb;
|
--net-speed-out-color: #89c3eb;
|
||||||
|
--conn-color: #90f2ff;
|
||||||
|
--conn-tcp-color: #89c3eb;
|
||||||
|
--conn-udp-color: #2ca9e1;
|
||||||
|
--load-color: #90f2ff;
|
||||||
|
--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;
|
||||||
--public-note-tag-color: #ddd;
|
--list-item-buy-link-color-hover: #ff9900;
|
||||||
--public-note-tag-bg: #6a7efc;
|
--public-note-tag-color: #ccc;
|
||||||
|
--public-note-tag-bg: linear-gradient(125deg, #676ef7, #41459c);
|
||||||
|
|
||||||
|
--option-high-color: #ff7500;
|
||||||
|
--option-high-color-active: rgba(255, 177, 0, 0.75);
|
||||||
|
|
||||||
|
--server-status-value-color: #a1eafb;
|
||||||
|
--server-status-label-color: #ddd;
|
||||||
|
--server-status-content-color: #eee;
|
||||||
|
|
||||||
// 针对1440px以下的屏幕
|
// 针对1440px以下的屏幕
|
||||||
@media screen and (max-width: 1440px) {
|
@media screen and (max-width: 1440px) {
|
||||||
@ -49,3 +71,27 @@
|
|||||||
--detail-container-width: 100vw;
|
--detail-container-width: 100vw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body.simple-color-mode {
|
||||||
|
|
||||||
|
--world-map-point-color: #cbf1f5;
|
||||||
|
|
||||||
|
--simple-color: #ccc;
|
||||||
|
--duration-color: var(--simple-color);
|
||||||
|
--transfer-color: var(--simple-color);
|
||||||
|
--transfer-in-color: var(--transfer-color);
|
||||||
|
--transfer-out-color: var(--simple-color);
|
||||||
|
--net-speed-in-color: var(--simple-color);
|
||||||
|
--net-speed-out-color: var(--simple-color);
|
||||||
|
|
||||||
|
--list-item-price-color: #eee;
|
||||||
|
--list-item-buy-link-color: var(--simple-color);
|
||||||
|
--list-item-buy-link-color-hover: draken(#cbf1f5, 10%);
|
||||||
|
--public-note-tag-color: #eee;
|
||||||
|
--public-note-tag-bg: transparent;
|
||||||
|
|
||||||
|
--option-high-color: rgb(93, 122, 126);
|
||||||
|
--option-high-color-active: rgba(93, 122, 126, 0.75);
|
||||||
|
|
||||||
|
--server-status-value-color: var(--simple-color);
|
||||||
|
}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { use } from 'echarts/core';
|
import { use } from 'echarts/core';
|
||||||
import { CanvasRenderer } from 'echarts/renderers';
|
import { SVGRenderer } from 'echarts/renderers';
|
||||||
import {
|
import {
|
||||||
BarChart,
|
BarChart,
|
||||||
} from 'echarts/charts';
|
} from 'echarts/charts';
|
||||||
@ -7,13 +7,40 @@ import {
|
|||||||
PolarComponent,
|
PolarComponent,
|
||||||
} from 'echarts/components';
|
} from 'echarts/components';
|
||||||
|
|
||||||
|
import config from '@/config';
|
||||||
|
|
||||||
use([
|
use([
|
||||||
CanvasRenderer,
|
SVGRenderer,
|
||||||
BarChart,
|
BarChart,
|
||||||
PolarComponent,
|
PolarComponent,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export default (used, total, itemColors, size = 100) => ({
|
function handleColor(color) {
|
||||||
|
if (Array.isArray(color)) {
|
||||||
|
return {
|
||||||
|
type: 'linear',
|
||||||
|
x: 1,
|
||||||
|
y: 1,
|
||||||
|
x2: 0,
|
||||||
|
y2: 0,
|
||||||
|
colorStops: [{
|
||||||
|
offset: 0,
|
||||||
|
color: color[0], // 0% 处的颜色
|
||||||
|
}, {
|
||||||
|
offset: 1,
|
||||||
|
color: color[1], // 100% 处的颜色
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return color;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default (used, total, itemColors, size = 100) => {
|
||||||
|
const isLinear = (
|
||||||
|
(config.nazhua.serverStatusLinear || config.nazhua.lightBackground)
|
||||||
|
&& !config.nazhua.simpleColorMode
|
||||||
|
);
|
||||||
|
return {
|
||||||
angleAxis: {
|
angleAxis: {
|
||||||
max: total, // 满分
|
max: total, // 满分
|
||||||
// 隐藏刻度线
|
// 隐藏刻度线
|
||||||
@ -56,11 +83,18 @@ export default (used, total, itemColors, size = 100) => ({
|
|||||||
value: used,
|
value: used,
|
||||||
}],
|
}],
|
||||||
itemStyle: {
|
itemStyle: {
|
||||||
color: typeof itemColors === 'string' ? itemColors : itemColors?.used,
|
color: typeof itemColors === 'string' ? itemColors : handleColor(itemColors?.used),
|
||||||
borderRadius: 5,
|
borderRadius: 5,
|
||||||
shadowColor: 'rgba(0, 0, 0, 0.2)',
|
shadowColor: (() => {
|
||||||
shadowBlur: 10,
|
if (config.nazhua.serverStatusLinear) {
|
||||||
shadowOffsetY: 2,
|
return 'rgba(0, 0, 0, 0.5)';
|
||||||
|
}
|
||||||
|
if (config.nazhua.lightBackground) {
|
||||||
|
return 'rgba(0, 0, 0, 0.2)';
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
})(),
|
||||||
|
shadowBlur: isLinear ? 10 : undefined,
|
||||||
},
|
},
|
||||||
coordinateSystem: 'polar',
|
coordinateSystem: 'polar',
|
||||||
cursor: 'default',
|
cursor: 'default',
|
||||||
@ -74,7 +108,7 @@ export default (used, total, itemColors, size = 100) => ({
|
|||||||
value: total,
|
value: total,
|
||||||
}],
|
}],
|
||||||
itemStyle: {
|
itemStyle: {
|
||||||
color: itemColors?.total || 'rgba(255, 255, 255, 0.2)',
|
color: handleColor(itemColors?.total) || 'rgba(255, 255, 255, 0.2)',
|
||||||
},
|
},
|
||||||
coordinateSystem: 'polar',
|
coordinateSystem: 'polar',
|
||||||
cursor: 'default',
|
cursor: 'default',
|
||||||
@ -82,4 +116,5 @@ export default (used, total, itemColors, size = 100) => ({
|
|||||||
barGap: '-100%', // 两环重叠
|
barGap: '-100%', // 两环重叠
|
||||||
z: 5,
|
z: 5,
|
||||||
}],
|
}],
|
||||||
});
|
};
|
||||||
|
};
|
||||||
|
|||||||
@ -1,28 +1,33 @@
|
|||||||
import { use } from 'echarts/core';
|
import { use } from 'echarts/core';
|
||||||
import { CanvasRenderer } from 'echarts/renderers';
|
import { SVGRenderer } from 'echarts/renderers';
|
||||||
import { LineChart } from 'echarts/charts';
|
import { LineChart } from 'echarts/charts';
|
||||||
import {
|
import {
|
||||||
TooltipComponent,
|
TooltipComponent,
|
||||||
LegendComponent,
|
|
||||||
GridComponent,
|
GridComponent,
|
||||||
|
DataZoomComponent,
|
||||||
} from 'echarts/components';
|
} from 'echarts/components';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
|
import config from '@/config';
|
||||||
|
|
||||||
use([
|
use([
|
||||||
CanvasRenderer,
|
SVGRenderer,
|
||||||
LineChart,
|
LineChart,
|
||||||
TooltipComponent,
|
TooltipComponent,
|
||||||
LegendComponent,
|
|
||||||
GridComponent,
|
GridComponent,
|
||||||
|
DataZoomComponent,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export default (
|
export default (options) => {
|
||||||
cateList,
|
const {
|
||||||
dateList,
|
dateList,
|
||||||
valueList,
|
valueList,
|
||||||
mode = 'dark',
|
mode = 'dark',
|
||||||
) => {
|
connectNulls = true,
|
||||||
|
} = options || {};
|
||||||
|
const fontFamily = config.nazhua.disableSarasaTermSC === true ? undefined : 'Sarasa Term SC';
|
||||||
const option = {
|
const option = {
|
||||||
|
darkMode: mode === 'dark',
|
||||||
tooltip: {
|
tooltip: {
|
||||||
trigger: 'axis',
|
trigger: 'axis',
|
||||||
axisPointer: {
|
axisPointer: {
|
||||||
@ -30,10 +35,31 @@ export default (
|
|||||||
},
|
},
|
||||||
formatter: (params) => {
|
formatter: (params) => {
|
||||||
const time = dayjs(parseInt(params[0].axisValue, 10)).format('YYYY.MM.DD HH:mm');
|
const time = dayjs(parseInt(params[0].axisValue, 10)).format('YYYY.MM.DD HH:mm');
|
||||||
let res = `${time}<br>`;
|
let res = `<p style="font-weight: bold; color: #ff6;">${time}</p>`;
|
||||||
|
if (params.length < 10) {
|
||||||
params.forEach((i) => {
|
params.forEach((i) => {
|
||||||
res += `${i.marker} ${i.seriesName}: ${i.value}ms<br>`;
|
res += i.value[1] ? `${i.marker} ${i.seriesName}: ${i.value[1]}ms<br>` : '';
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
res += '<table>';
|
||||||
|
let trEnd = false;
|
||||||
|
params.forEach((i, index) => {
|
||||||
|
if (index % 2 === 0) {
|
||||||
|
res += '<tr>';
|
||||||
|
}
|
||||||
|
res += i.value[1]
|
||||||
|
? `<td style="padding: 0 4px;">${i.marker} ${i.seriesName}: ${i.value[1]}ms</td>`
|
||||||
|
: '<td style="padding: 0 4px;"></td>';
|
||||||
|
if (index % 2 === 1) {
|
||||||
|
res += '</tr>';
|
||||||
|
trEnd = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!trEnd) {
|
||||||
|
res += '</tr>';
|
||||||
|
}
|
||||||
|
res += '</table>';
|
||||||
|
}
|
||||||
return res;
|
return res;
|
||||||
},
|
},
|
||||||
backgroundColor: mode === 'dark' ? 'rgba(0, 0, 0, 0.7)' : 'rgba(255, 255, 255, 0.7)',
|
backgroundColor: mode === 'dark' ? 'rgba(0, 0, 0, 0.7)' : 'rgba(255, 255, 255, 0.7)',
|
||||||
@ -44,32 +70,19 @@ export default (
|
|||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
legend: {
|
grid: {
|
||||||
top: 5,
|
top: 10,
|
||||||
data: cateList,
|
left: 5,
|
||||||
textStyle: {
|
right: 5,
|
||||||
color: mode === 'dark' ? '#ddd' : '#222',
|
bottom: 50,
|
||||||
fontFamily: 'Sarasa Term SC',
|
containLabel: true,
|
||||||
fontSize: 14,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
xAxis: {
|
|
||||||
type: 'category',
|
|
||||||
data: dateList,
|
|
||||||
axisLabel: {
|
|
||||||
hideOverlap: true,
|
|
||||||
interval: Math.max(
|
|
||||||
Math.ceil(dateList.length / 12),
|
|
||||||
1,
|
|
||||||
),
|
|
||||||
nameTextStyle: {
|
|
||||||
fontSize: 12,
|
|
||||||
},
|
|
||||||
formatter: (val) => dayjs(parseInt(val, 10)).format('HH:mm'),
|
|
||||||
fontFamily: 'Sarasa Term SC',
|
|
||||||
color: mode === 'dark' ? '#eee' : '#222',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
dataZoom: [{
|
||||||
|
id: 'dataZoomX',
|
||||||
|
type: 'slider',
|
||||||
|
xAxisIndex: [0],
|
||||||
|
filterMode: 'filter',
|
||||||
|
}],
|
||||||
yAxis: {
|
yAxis: {
|
||||||
type: 'value',
|
type: 'value',
|
||||||
splitLine: {
|
splitLine: {
|
||||||
@ -78,23 +91,28 @@ export default (
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
axisLabel: {
|
axisLabel: {
|
||||||
fontFamily: 'Sarasa Term SC',
|
fontFamily,
|
||||||
color: mode === 'dark' ? '#ddd' : '#222',
|
color: mode === 'dark' ? '#ddd' : '#222',
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
grid: {
|
xAxis: {
|
||||||
left: 0,
|
type: 'time',
|
||||||
right: 0,
|
data: dateList,
|
||||||
bottom: 0,
|
axisLabel: {
|
||||||
containLabel: true,
|
hideOverlap: true,
|
||||||
|
nameTextStyle: {
|
||||||
|
fontSize: 12,
|
||||||
|
},
|
||||||
|
fontFamily,
|
||||||
|
color: mode === 'dark' ? '#eee' : '#222',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
series: valueList.map((i) => ({
|
series: valueList.map((i) => ({
|
||||||
|
...i,
|
||||||
type: 'line',
|
type: 'line',
|
||||||
data: i.data,
|
|
||||||
name: i.name,
|
|
||||||
smooth: true,
|
smooth: true,
|
||||||
connectNulls: true,
|
connectNulls,
|
||||||
legendHoverLink: false,
|
legendHoverLink: false,
|
||||||
symbol: 'none',
|
symbol: 'none',
|
||||||
})),
|
})),
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
<div
|
<div
|
||||||
v-if="option"
|
v-if="option"
|
||||||
class="line-box"
|
class="line-box"
|
||||||
|
:style="boxStyle"
|
||||||
>
|
>
|
||||||
<v-chart
|
<v-chart
|
||||||
ref="chartRef"
|
ref="chartRef"
|
||||||
@ -25,10 +26,6 @@ import VChart from 'vue-echarts';
|
|||||||
import lineChart from './line';
|
import lineChart from './line';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
cateList: {
|
|
||||||
type: Array,
|
|
||||||
default: () => [],
|
|
||||||
},
|
|
||||||
dateList: {
|
dateList: {
|
||||||
type: Array,
|
type: Array,
|
||||||
default: () => [],
|
default: () => [],
|
||||||
@ -41,19 +38,30 @@ const props = defineProps({
|
|||||||
type: [Number, String],
|
type: [Number, String],
|
||||||
default: null,
|
default: null,
|
||||||
},
|
},
|
||||||
|
connectNulls: {
|
||||||
|
type: [Boolean, String],
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const chartRef = ref();
|
const chartRef = ref();
|
||||||
const option = computed(() => {
|
const option = computed(() => {
|
||||||
if (props.cateList && props.dateList && props.valueList) {
|
if (props.dateList && props.valueList) {
|
||||||
return lineChart(
|
return lineChart({
|
||||||
props.cateList,
|
dateList: props.dateList,
|
||||||
props.dateList,
|
valueList: props.valueList,
|
||||||
props.valueList,
|
connectNulls: props.connectNulls,
|
||||||
);
|
});
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
|
const boxStyle = computed(() => {
|
||||||
|
const style = {};
|
||||||
|
if (props.size > 0) {
|
||||||
|
style.height = `${props.size}px`;
|
||||||
|
}
|
||||||
|
return style;
|
||||||
|
});
|
||||||
|
|
||||||
function handleResize() {
|
function handleResize() {
|
||||||
chartRef.value?.resize?.();
|
chartRef.value?.resize?.();
|
||||||
|
|||||||
93
src/components/dot-dot-box.vue
Normal file
93
src/components/dot-dot-box.vue
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="dot-dot-box"
|
||||||
|
:class="{
|
||||||
|
'dot-dot-box--hide': hideDotBG,
|
||||||
|
}"
|
||||||
|
:style="boxStyle"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
/**
|
||||||
|
* 点格子背景盒子
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import config from '@/config';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
borderRadius: {
|
||||||
|
type: [String, Number],
|
||||||
|
default: 12,
|
||||||
|
},
|
||||||
|
padding: {
|
||||||
|
type: [String, Number],
|
||||||
|
default: 20,
|
||||||
|
},
|
||||||
|
color: {
|
||||||
|
type: String,
|
||||||
|
default: '#eee',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const lightBackground = computed(() => config.nazhua.lightBackground);
|
||||||
|
|
||||||
|
const hideDotBG = computed(() => lightBackground.value || config.nazhua?.hideDotBG === true);
|
||||||
|
|
||||||
|
const boxStyle = computed(() => {
|
||||||
|
const style = {};
|
||||||
|
if (props.borderRadius) {
|
||||||
|
if (typeof props.borderRadius === 'number') {
|
||||||
|
style['--border-radius'] = `${props.borderRadius}px`;
|
||||||
|
} else {
|
||||||
|
style['--border-radius'] = `${props.borderRadius}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (props.padding) {
|
||||||
|
if (typeof props.padding === 'number') {
|
||||||
|
style.padding = `${props.padding}px`;
|
||||||
|
} else {
|
||||||
|
style.padding = props.padding;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (props.color) {
|
||||||
|
style.color = props.color;
|
||||||
|
}
|
||||||
|
return style;
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.dot-dot-box {
|
||||||
|
--border-radius: 12px;
|
||||||
|
color: #eee;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
box-shadow: 2px 4px 6px rgba(#000, 0.4);
|
||||||
|
|
||||||
|
background-image: radial-gradient(transparent 1px, rgba(#000, 0.6) 1px);
|
||||||
|
background-size: 3px 3px;
|
||||||
|
backdrop-filter: saturate(50%) blur(3px);
|
||||||
|
|
||||||
|
&--hide {
|
||||||
|
background-color: rgba(#000, 0.5);
|
||||||
|
background-image: none;
|
||||||
|
backdrop-filter: none;
|
||||||
|
transition: all 0.3s linear;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: rgba(#000, 0.8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 768px) {
|
||||||
|
background-color: rgba(#000, 0.8);
|
||||||
|
background-image: none;
|
||||||
|
backdrop-filter: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
158
src/components/fireworks.vue
Normal file
158
src/components/fireworks.vue
Normal file
@ -0,0 +1,158 @@
|
|||||||
|
<template>
|
||||||
|
<canvas
|
||||||
|
ref="canvas"
|
||||||
|
class="fireworks-canvas"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import {
|
||||||
|
ref,
|
||||||
|
onMounted,
|
||||||
|
onUnmounted,
|
||||||
|
} from 'vue';
|
||||||
|
|
||||||
|
const canvas = ref(null);
|
||||||
|
let ctx = null;
|
||||||
|
let particles = [];
|
||||||
|
let rockets = [];
|
||||||
|
let animationFrameId = null;
|
||||||
|
|
||||||
|
class Particle {
|
||||||
|
constructor(x, y, color) {
|
||||||
|
this.x = x;
|
||||||
|
this.y = y;
|
||||||
|
this.color = color;
|
||||||
|
this.velocity = {
|
||||||
|
x: (Math.random() - 0.5) * 8,
|
||||||
|
y: (Math.random() - 0.5) * 12 - 8,
|
||||||
|
};
|
||||||
|
this.alpha = 1;
|
||||||
|
this.decay = 0.02;
|
||||||
|
}
|
||||||
|
|
||||||
|
draw() {
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(this.x, this.y, 2, 0, Math.PI * 2);
|
||||||
|
ctx.fillStyle = `rgba(${this.color}, ${this.alpha})`;
|
||||||
|
ctx.fill();
|
||||||
|
}
|
||||||
|
|
||||||
|
update() {
|
||||||
|
this.velocity.y += 0.1;
|
||||||
|
this.x += this.velocity.x;
|
||||||
|
this.y += this.velocity.y;
|
||||||
|
this.alpha -= this.decay;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createFirework(x, y) {
|
||||||
|
const colors = [
|
||||||
|
'255, 0, 0',
|
||||||
|
'0, 255, 0',
|
||||||
|
'0, 0, 255',
|
||||||
|
'255, 255, 0',
|
||||||
|
'255, 0, 255',
|
||||||
|
'0, 255, 255',
|
||||||
|
];
|
||||||
|
const color = colors[Math.floor(Math.random() * colors.length)];
|
||||||
|
for (let i = 0; i < 80; i += 1) {
|
||||||
|
particles.push(new Particle(x, y, color));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Rocket {
|
||||||
|
constructor() {
|
||||||
|
this.x = Math.random() * canvas.value.width;
|
||||||
|
this.y = canvas.value.height;
|
||||||
|
this.targetY = canvas.value.height * 0.5;
|
||||||
|
this.speed = 15;
|
||||||
|
this.trail = [];
|
||||||
|
this.maxTrailLength = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
draw() {
|
||||||
|
// 绘制火箭尾迹
|
||||||
|
ctx.beginPath();
|
||||||
|
this.trail.forEach((pos, index) => {
|
||||||
|
ctx.fillStyle = `rgba(255, 200, 0, ${index / this.trail.length})`;
|
||||||
|
ctx.fillRect(pos.x, pos.y, 2, 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 绘制火箭本体
|
||||||
|
ctx.fillStyle = 'rgba(255, 220, 0, 1)';
|
||||||
|
ctx.fillRect(this.x, this.y, 3, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
update() {
|
||||||
|
this.trail.push({
|
||||||
|
x: this.x,
|
||||||
|
y: this.y,
|
||||||
|
});
|
||||||
|
if (this.trail.length > this.maxTrailLength) {
|
||||||
|
this.trail.shift();
|
||||||
|
}
|
||||||
|
this.y -= this.speed;
|
||||||
|
if (this.y <= this.targetY) {
|
||||||
|
createFirework(this.x, this.y);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function animate() {
|
||||||
|
ctx.clearRect(0, 0, canvas.value.width, canvas.value.height);
|
||||||
|
|
||||||
|
// 更新和绘制火箭
|
||||||
|
rockets = rockets.filter((rocket) => {
|
||||||
|
rocket.draw();
|
||||||
|
return rocket.update();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 更新和绘制粒子
|
||||||
|
particles = particles.filter((particle) => particle.alpha > 0);
|
||||||
|
particles.forEach((particle) => {
|
||||||
|
particle.draw();
|
||||||
|
particle.update();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 发射新的火箭
|
||||||
|
if (Math.random() < 0.03 && rockets.length < 3) {
|
||||||
|
rockets.push(new Rocket());
|
||||||
|
}
|
||||||
|
|
||||||
|
animationFrameId = requestAnimationFrame(animate);
|
||||||
|
}
|
||||||
|
|
||||||
|
function resizeCanvas() {
|
||||||
|
if (canvas.value) {
|
||||||
|
canvas.value.width = window.innerWidth;
|
||||||
|
canvas.value.height = window.innerHeight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
ctx = canvas.value.getContext('2d');
|
||||||
|
resizeCanvas();
|
||||||
|
window.addEventListener('resize', resizeCanvas);
|
||||||
|
animate();
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
window.removeEventListener('resize', resizeCanvas);
|
||||||
|
if (animationFrameId) {
|
||||||
|
cancelAnimationFrame(animationFrameId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.fireworks-canvas {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 8;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
257
src/components/lantern.vue
Normal file
257
src/components/lantern.vue
Normal file
@ -0,0 +1,257 @@
|
|||||||
|
<template>
|
||||||
|
<div class="lantern-container">
|
||||||
|
<div class="lantern-group right-group">
|
||||||
|
<div class="deng-box">
|
||||||
|
<div class="deng">
|
||||||
|
<div class="xian" />
|
||||||
|
<div class="deng-a">
|
||||||
|
<div class="deng-b">
|
||||||
|
<div class="deng-t">快</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="shui shui-a">
|
||||||
|
<div class="shui-c" />
|
||||||
|
<div class="shui-b" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="deng-box deng-box--2">
|
||||||
|
<div class="deng">
|
||||||
|
<div class="xian" />
|
||||||
|
<div class="deng-a">
|
||||||
|
<div class="deng-b">
|
||||||
|
<div class="deng-t">乐</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="shui shui-a">
|
||||||
|
<div class="shui-c" />
|
||||||
|
<div class="shui-b" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="lantern-group left-group">
|
||||||
|
<div class="deng-box">
|
||||||
|
<div class="deng">
|
||||||
|
<div class="xian" />
|
||||||
|
<div class="deng-a">
|
||||||
|
<div class="deng-b">
|
||||||
|
<div class="deng-t">新</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="shui shui-a">
|
||||||
|
<div class="shui-c" />
|
||||||
|
<div class="shui-b" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="deng-box deng-box--2">
|
||||||
|
<div class="deng">
|
||||||
|
<div class="xian" />
|
||||||
|
<div class="deng-a">
|
||||||
|
<div class="deng-b">
|
||||||
|
<div class="deng-t">年</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="shui shui-a">
|
||||||
|
<div class="shui-c" />
|
||||||
|
<div class="shui-b" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
// 灯笼组件
|
||||||
|
// 由AI生成
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.lantern-container {
|
||||||
|
position: fixed;
|
||||||
|
top: calc(var(--layout-header-height) + 5px);
|
||||||
|
width: 100%;
|
||||||
|
z-index: 50;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lantern-group {
|
||||||
|
position: fixed;
|
||||||
|
top: 70px;
|
||||||
|
animation: swing 3s infinite ease-in-out;
|
||||||
|
transform-origin: 50% -10px;
|
||||||
|
|
||||||
|
&.left-group {
|
||||||
|
left: 40px;
|
||||||
|
animation-delay: -1.5s;
|
||||||
|
|
||||||
|
.deng-box:nth-child(2) {
|
||||||
|
margin-top: -12px;
|
||||||
|
|
||||||
|
.deng {
|
||||||
|
animation: swing-extra 2s infinite ease-in-out;
|
||||||
|
animation-delay: -0.5s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.right-group {
|
||||||
|
right: 30px;
|
||||||
|
animation-delay: -0.5s;
|
||||||
|
|
||||||
|
.deng-box:nth-child(2) {
|
||||||
|
|
||||||
|
.deng {
|
||||||
|
animation: swing-extra 2s infinite ease-in-out;
|
||||||
|
animation-delay: -1s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.deng {
|
||||||
|
animation: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deng-box {
|
||||||
|
position: relative;
|
||||||
|
top: -40px;
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
z-index: 2;
|
||||||
|
.deng {
|
||||||
|
margin-bottom: 23px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:nth-child(2) {
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.deng {
|
||||||
|
position: relative;
|
||||||
|
width: 120px;
|
||||||
|
height: 90px;
|
||||||
|
margin: 50px;
|
||||||
|
background: rgba(216, 0, 15, 0.8);
|
||||||
|
border-radius: 50% 50%;
|
||||||
|
transform-origin: 50% -100px;
|
||||||
|
animation: swing 3s infinite ease-in-out;
|
||||||
|
box-shadow: -5px 5px 50px 4px rgba(250, 108, 0, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.deng-a {
|
||||||
|
width: 100px;
|
||||||
|
height: 90px;
|
||||||
|
background: rgba(216, 0, 15, 0.1);
|
||||||
|
margin: 12px 8px 8px 10px;
|
||||||
|
border-radius: 50% 50%;
|
||||||
|
border: 2px solid #dc8f03;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deng-b {
|
||||||
|
width: 45px;
|
||||||
|
height: 90px;
|
||||||
|
background: rgba(216, 0, 15, 0.1);
|
||||||
|
margin: -4px 8px 8px 26px;
|
||||||
|
border-radius: 50% 50%;
|
||||||
|
border: 2px solid #dc8f03;
|
||||||
|
}
|
||||||
|
|
||||||
|
.xian {
|
||||||
|
position: absolute;
|
||||||
|
top: -20px;
|
||||||
|
left: 60px;
|
||||||
|
width: 2px;
|
||||||
|
height: 20px;
|
||||||
|
background: #dc8f03;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shui-a {
|
||||||
|
position: relative;
|
||||||
|
width: 5px;
|
||||||
|
height: 20px;
|
||||||
|
margin: -5px 0 0 59px;
|
||||||
|
transform-origin: 50% -45px;
|
||||||
|
background: #ffa500;
|
||||||
|
border-radius: 0 0 5px 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shui-b {
|
||||||
|
position: absolute;
|
||||||
|
top: 14px;
|
||||||
|
left: -2px;
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
background: #dc8f03;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shui-c {
|
||||||
|
position: absolute;
|
||||||
|
top: 18px;
|
||||||
|
left: -2px;
|
||||||
|
width: 10px;
|
||||||
|
height: 35px;
|
||||||
|
background: #ffa500;
|
||||||
|
border-radius: 0 0 0 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deng:before {
|
||||||
|
position: absolute;
|
||||||
|
top: -7px;
|
||||||
|
left: 29px;
|
||||||
|
height: 12px;
|
||||||
|
width: 60px;
|
||||||
|
content: " ";
|
||||||
|
display: block;
|
||||||
|
z-index: 999;
|
||||||
|
border-radius: 5px 5px 0 0;
|
||||||
|
border: solid 1px #dc8f03;
|
||||||
|
background: linear-gradient(to right, #dc8f03, #ffa500, #dc8f03, #ffa500, #dc8f03);
|
||||||
|
}
|
||||||
|
|
||||||
|
.deng:after {
|
||||||
|
position: absolute;
|
||||||
|
bottom: -7px;
|
||||||
|
left: 10px;
|
||||||
|
height: 12px;
|
||||||
|
width: 60px;
|
||||||
|
content: " ";
|
||||||
|
display: block;
|
||||||
|
margin-left: 20px;
|
||||||
|
border-radius: 0 0 5px 5px;
|
||||||
|
border: solid 1px #dc8f03;
|
||||||
|
background: linear-gradient(to right, #dc8f03, #ffa500, #dc8f03, #ffa500, #dc8f03);
|
||||||
|
}
|
||||||
|
|
||||||
|
.deng-t {
|
||||||
|
font-family: 华文行楷, Arial, Lucida Grande, Tahoma, sans-serif;
|
||||||
|
font-size: 3.2rem;
|
||||||
|
color: #ffd000;
|
||||||
|
line-height: 85px;
|
||||||
|
text-align: center;
|
||||||
|
margin-left: -5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes swing {
|
||||||
|
0% { transform: rotate(-6deg) }
|
||||||
|
50% { transform: rotate(6deg) }
|
||||||
|
100% { transform: rotate(-6deg) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes swing-extra {
|
||||||
|
0% { transform: rotate(-3deg) }
|
||||||
|
50% { transform: rotate(3deg) }
|
||||||
|
100% { transform: rotate(-3deg) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 1024px) {
|
||||||
|
.lantern-container {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
292
src/components/popover.vue
Normal file
292
src/components/popover.vue
Normal file
@ -0,0 +1,292 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
ref="triggerRef"
|
||||||
|
class="popover-trigger"
|
||||||
|
@mouseenter="handleMouseEnter"
|
||||||
|
@mouseleave="handleMouseLeave"
|
||||||
|
@focusin="handleFocusIn"
|
||||||
|
@focusout="handleFocusOut"
|
||||||
|
@click="handleTriggerClick"
|
||||||
|
>
|
||||||
|
<slot name="trigger" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Teleport to="body">
|
||||||
|
<div
|
||||||
|
v-show="isShow"
|
||||||
|
ref="popoverRef"
|
||||||
|
class="popover"
|
||||||
|
:style="[popoverStyle, { zIndex: currentZIndex }]"
|
||||||
|
>
|
||||||
|
<template v-if="$slots.title || title">
|
||||||
|
<div class="popover-body">
|
||||||
|
{{ title }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<div class="popover-body">
|
||||||
|
<slot name="default" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</Teleport>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
/**
|
||||||
|
组件名称:Popover
|
||||||
|
|
||||||
|
组件说明:
|
||||||
|
该组件在移动端与 PC 端提供不同的交互模式,通过 "hover" 或 "click" 来触发显示或隐藏提示浮层。
|
||||||
|
若设置 unique 属性,则在显示新浮层的同时会隐藏其他已显示的浮层。
|
||||||
|
|
||||||
|
使用示例:
|
||||||
|
<Popover title="示例标题" trigger="click">
|
||||||
|
<template #trigger>
|
||||||
|
<button>点击触发</button>
|
||||||
|
</template>
|
||||||
|
这是 Popover 的内容
|
||||||
|
</Popover>
|
||||||
|
|
||||||
|
Props:
|
||||||
|
- visible (Boolean,默认 false)
|
||||||
|
Popover 的可见状态,可供外部进行手动控制。
|
||||||
|
- title (String,默认 '')
|
||||||
|
Popover 的标题文本,如不传则展示默认内容插槽。
|
||||||
|
- trigger (String,默认 'hover')
|
||||||
|
触发模式,可选值为 "hover" 或 "click"。
|
||||||
|
- unique (Boolean,默认 true)
|
||||||
|
如果为 true,则在显示当前 Popover 时会自动隐藏其他已显示的 Popover。
|
||||||
|
|
||||||
|
方法说明:
|
||||||
|
- handleMouseEnter()
|
||||||
|
当鼠标移入触发元素时,若 trigger 为 hover,会显示 Popover。
|
||||||
|
- handleMouseLeave()
|
||||||
|
当鼠标移出触发元素时,若 trigger 为 hover,会隐藏 Popover。
|
||||||
|
- handleTriggerClick(e)
|
||||||
|
当在移动端或 trigger 为 click 时,点击触发元素会切换 Popover 显示状态,并在移动端下自动延时隐藏。
|
||||||
|
- handleFocusIn()
|
||||||
|
当触发元素获得焦点时,若触发方式为 hover,会显示 Popover。
|
||||||
|
- handleFocusOut()
|
||||||
|
当触发元素失去焦点时,若触发方式为 hover,会隐藏 Popover。
|
||||||
|
|
||||||
|
注意事项:
|
||||||
|
- 在移动端会根据窗口宽度做适配,通过 document 监听点击事件和窗口大小变化来控制显示与关闭。
|
||||||
|
- 当 visible 通过外部控制时,非移动端能手动实现 Popover 的显隐。
|
||||||
|
*/
|
||||||
|
import {
|
||||||
|
ref,
|
||||||
|
computed,
|
||||||
|
onMounted,
|
||||||
|
onUnmounted,
|
||||||
|
watch,
|
||||||
|
} from 'vue';
|
||||||
|
import { getNextZIndex } from '../utils/zIndexManager';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
visible: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
trigger: {
|
||||||
|
type: String,
|
||||||
|
default: 'hover',
|
||||||
|
validator: (value) => ['hover', 'click'].includes(value),
|
||||||
|
},
|
||||||
|
unique: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 移除全局 Symbol 相关代码
|
||||||
|
// 添加静态 z-index 计数器
|
||||||
|
// const baseZIndex = 1000;
|
||||||
|
// let zIndexCounter = baseZIndex;
|
||||||
|
|
||||||
|
const popoverRef = ref(null);
|
||||||
|
const position = ref({
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
});
|
||||||
|
const isMobile = ref(window.innerWidth < 600);
|
||||||
|
const isShow = ref(false);
|
||||||
|
const triggerRef = ref(null);
|
||||||
|
const currentZIndex = ref(1000);
|
||||||
|
|
||||||
|
// 移除 getCurrentPopover 和 setCurrentPopover 函数
|
||||||
|
|
||||||
|
// 更新移动端位置
|
||||||
|
const updateMobilePosition = () => {
|
||||||
|
if (!triggerRef.value) return;
|
||||||
|
const rect = triggerRef.value.getBoundingClientRect();
|
||||||
|
position.value = {
|
||||||
|
x: rect.left + rect.width / 2,
|
||||||
|
y: rect.top + rect.height,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// 修改显示逻辑
|
||||||
|
const updateShow = (value) => {
|
||||||
|
if (value) {
|
||||||
|
currentZIndex.value = getNextZIndex();
|
||||||
|
}
|
||||||
|
isShow.value = value;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseEnter = () => {
|
||||||
|
if (!isMobile.value && props.trigger === 'hover') {
|
||||||
|
updateShow(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseLeave = () => {
|
||||||
|
if (!isMobile.value && props.trigger === 'hover') {
|
||||||
|
updateShow(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let autoCloseTimer;
|
||||||
|
const handleTriggerClick = (e) => {
|
||||||
|
if (props.trigger === 'click' || isMobile.value) {
|
||||||
|
e.stopPropagation();
|
||||||
|
updateShow(!isShow.value);
|
||||||
|
if (isShow.value && isMobile.value) {
|
||||||
|
if (autoCloseTimer) {
|
||||||
|
clearTimeout(autoCloseTimer);
|
||||||
|
}
|
||||||
|
autoCloseTimer = setTimeout(() => {
|
||||||
|
isShow.value = false;
|
||||||
|
}, 5 * 1000);
|
||||||
|
updateMobilePosition();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFocusIn = () => {
|
||||||
|
if (!isMobile.value && props.trigger === 'hover') {
|
||||||
|
isShow.value = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFocusOut = () => {
|
||||||
|
if (!isMobile.value && props.trigger === 'hover') {
|
||||||
|
isShow.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 修改点击事件处理
|
||||||
|
const handleDocumentClick = (e) => {
|
||||||
|
if (isShow.value && !triggerRef.value?.contains(e.target) && !popoverRef.value?.contains(e.target)) {
|
||||||
|
isShow.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const updatePosition = (e) => {
|
||||||
|
if (isMobile.value || !isShow.value) return;
|
||||||
|
position.value = {
|
||||||
|
x: e.clientX,
|
||||||
|
y: e.clientY,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const popoverStyle = computed(() => {
|
||||||
|
if (isMobile.value) {
|
||||||
|
return {
|
||||||
|
position: 'fixed',
|
||||||
|
bottom: '10vh',
|
||||||
|
left: '50%',
|
||||||
|
transform: 'translateX(-50%)',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const { x, y } = position.value;
|
||||||
|
const rect = popoverRef.value?.getBoundingClientRect();
|
||||||
|
const offset = 15; // 修改为20px偏移量
|
||||||
|
|
||||||
|
let left = x + offset;
|
||||||
|
let top = y + offset;
|
||||||
|
|
||||||
|
if (rect) {
|
||||||
|
// 防止超出右边界
|
||||||
|
if (left + rect.width > window.innerWidth) {
|
||||||
|
left = x - rect.width - offset;
|
||||||
|
}
|
||||||
|
// 防止超出下边界
|
||||||
|
if (top + rect.height > window.innerHeight) {
|
||||||
|
top = y - rect.height - offset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
position: 'fixed',
|
||||||
|
left: `${left}px`,
|
||||||
|
top: `${top}px`,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleResize = () => {
|
||||||
|
isMobile.value = window.innerWidth < 600;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 监听visible属性变化
|
||||||
|
watch(() => props.visible, (newVal) => {
|
||||||
|
if (!isMobile.value) {
|
||||||
|
updateShow(newVal);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (isMobile.value || props.trigger === 'click') {
|
||||||
|
document.addEventListener('click', handleDocumentClick);
|
||||||
|
}
|
||||||
|
if (!isMobile.value) {
|
||||||
|
document.addEventListener('mousemove', updatePosition);
|
||||||
|
}
|
||||||
|
window.addEventListener('resize', handleResize);
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (isMobile.value || props.trigger === 'click') {
|
||||||
|
document.removeEventListener('click', handleDocumentClick);
|
||||||
|
}
|
||||||
|
if (!isMobile.value) {
|
||||||
|
document.removeEventListener('mousemove', updatePosition);
|
||||||
|
}
|
||||||
|
window.removeEventListener('resize', handleResize);
|
||||||
|
// 移除全局 Popover 相关的清理代码
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.popover-trigger {
|
||||||
|
display: inline-block;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popover {
|
||||||
|
background: rgba(#000, 0.8);
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.15);
|
||||||
|
// 移除固定的 z-index
|
||||||
|
max-width: 300px;
|
||||||
|
|
||||||
|
@media screen and (max-width: 600px) {
|
||||||
|
max-width: 90%;
|
||||||
|
text-align: center;
|
||||||
|
box-shadow: 0 4px 12px rgba(251, 255, 217, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.popover-body {
|
||||||
|
line-height: 1.4;
|
||||||
|
font-size: 14px;
|
||||||
|
// 允许换行
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
29
src/components/server-flag.vue
Normal file
29
src/components/server-flag.vue
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
<template>
|
||||||
|
<span
|
||||||
|
class="server-flag"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="fi"
|
||||||
|
:class="'fi-' + lastFlag"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
info: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const lastFlag = computed(() => {
|
||||||
|
let flag = props.info?.Host?.CountryCode || 'un';
|
||||||
|
if (props.info?.PublicNote?.customData?.flag) {
|
||||||
|
flag = props.info.PublicNote.customData.flag;
|
||||||
|
}
|
||||||
|
return flag.toLowerCase();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@ -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"
|
||||||
@ -58,6 +59,9 @@ function handleClick() {
|
|||||||
width: 16px;
|
width: 16px;
|
||||||
height: 16px;
|
height: 16px;
|
||||||
transform: translate(-50%, -50%);
|
transform: translate(-50%, -50%);
|
||||||
|
:hover {
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
.point-block {
|
.point-block {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@ -85,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>
|
||||||
|
|||||||
@ -1,6 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="world-map-group"
|
class="world-map-group"
|
||||||
|
:class="{
|
||||||
|
'world-map-group--light-background': lightBackground,
|
||||||
|
}"
|
||||||
:style="mapStyle"
|
:style="mapStyle"
|
||||||
>
|
>
|
||||||
<div class="world-map-img" />
|
<div class="world-map-img" />
|
||||||
@ -37,10 +40,15 @@
|
|||||||
import {
|
import {
|
||||||
ref,
|
ref,
|
||||||
computed,
|
computed,
|
||||||
|
watch,
|
||||||
} from 'vue';
|
} from 'vue';
|
||||||
|
import config from '@/config';
|
||||||
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: {
|
||||||
@ -59,37 +67,46 @@ const props = defineProps({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const lightBackground = computed(() => config.nazhua.lightBackground);
|
||||||
|
const boxPadding = computed(() => (lightBackground.value ? 20 : 0));
|
||||||
|
|
||||||
// 计算地图大小 保持1280:621的比例 保证地图不变形
|
// 计算地图大小 保持1280:621的比例 保证地图不变形
|
||||||
const computedSize = computed(() => {
|
const computedSize = computed(() => {
|
||||||
|
// 考虑内边距,从总宽高中减去padding
|
||||||
|
const adjustedWidth = Number(props.width) - (boxPadding.value * 2);
|
||||||
|
const adjustedHeight = Number(props.height) - (boxPadding.value * 2);
|
||||||
|
|
||||||
if (!validate.isEmpty(props.width) && !validate.isEmpty(props.height)) {
|
if (!validate.isEmpty(props.width) && !validate.isEmpty(props.height)) {
|
||||||
return {
|
return {
|
||||||
width: 1280,
|
width: 1280,
|
||||||
height: 621,
|
height: 621,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const width = Number(props.width);
|
|
||||||
const height = Number(props.height);
|
|
||||||
if (!validate.isEmpty(props.width) && validate.isEmpty(props.height)) {
|
if (!validate.isEmpty(props.width) && validate.isEmpty(props.height)) {
|
||||||
return {
|
return {
|
||||||
width,
|
width: adjustedWidth,
|
||||||
height: Math.ceil((621 / 1280) * width),
|
height: Math.ceil((621 / 1280) * adjustedWidth),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (validate.isEmpty(props.width) && !validate.isEmpty(props.height)) {
|
if (validate.isEmpty(props.width) && !validate.isEmpty(props.height)) {
|
||||||
return {
|
return {
|
||||||
width: Math.ceil((1280 / 621) * height),
|
width: Math.ceil((1280 / 621) * adjustedHeight),
|
||||||
height,
|
height: adjustedHeight,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (width / height > 1280 / 621) {
|
|
||||||
|
if (adjustedWidth / adjustedHeight > 1280 / 621) {
|
||||||
return {
|
return {
|
||||||
width: Math.ceil(height * (1280 / 621)),
|
width: Math.ceil(adjustedHeight * (1280 / 621)),
|
||||||
height,
|
height: adjustedHeight,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
width,
|
width: adjustedWidth,
|
||||||
height: Math.ceil(width * (621 / 1280)),
|
height: Math.ceil(adjustedWidth * (621 / 1280)),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -100,16 +117,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 + boxPadding.value,
|
||||||
top: (computedSize.value.height / 621) * i.y,
|
top: (computedSize.value.height / 621) * i.y + boxPadding.value,
|
||||||
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,26 +201,26 @@ 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 = '4px';
|
||||||
style.left = '50%';
|
style.left = '50%';
|
||||||
style.transform = 'translate(-50%, 0)';
|
style.transform = 'translate(-50%, 0)';
|
||||||
}
|
}
|
||||||
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);
|
||||||
}
|
}
|
||||||
@ -156,6 +232,30 @@ function handlePointTap(e) {
|
|||||||
height: var(--world-map-height, 621px);
|
height: var(--world-map-height, 621px);
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
|
&--light-background {
|
||||||
|
padding: 20px;
|
||||||
|
background: rgba(#000, 0.6);
|
||||||
|
border-radius: 12px;
|
||||||
|
box-sizing: content-box;
|
||||||
|
transition: background-color 0.3s linear;
|
||||||
|
|
||||||
|
.world-map-img {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(#000, 0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 768px) {
|
||||||
|
background: rgba(#000, 0.8);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(#000, 0.8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.world-map-img {
|
.world-map-img {
|
||||||
width: var(--world-map-width, 1280px);
|
width: var(--world-map-width, 1280px);
|
||||||
height: var(--world-map-height, 621px);
|
height: var(--world-map-height, 621px);
|
||||||
@ -169,10 +269,33 @@ function handlePointTap(e) {
|
|||||||
padding: 5px 10px;
|
padding: 5px 10px;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
line-height: 20px;
|
line-height: 20px;
|
||||||
|
white-space: pre;
|
||||||
color: #eee;
|
color: #eee;
|
||||||
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;
|
||||||
|
|
||||||
|
// 向上的尖角
|
||||||
|
&::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%);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 500px) {
|
||||||
|
line-height: 16px;
|
||||||
|
font-size: 12px;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,30 +1,87 @@
|
|||||||
const config = {
|
import {
|
||||||
request: {
|
reactive,
|
||||||
headers: {
|
} from 'vue';
|
||||||
// 如果设置的是json请求。api的defaultContentType为false的时候,contentType为form请求,反之亦如此
|
import {
|
||||||
'Content-Type': 'application/json',
|
loadProfile as loadNezhaV1Profile,
|
||||||
},
|
} from '@/utils/load-nezha-v1-config';
|
||||||
codeField: 'code', // code字段
|
|
||||||
dataField: 'result', // 数据字段
|
const defaultNezhaVersion = import.meta.env.VITE_NEZHA_VERSION;
|
||||||
msgField: 'message', // 消息字段
|
|
||||||
okCode: '0', // 数据通过code
|
const config = reactive({
|
||||||
limit: 10,
|
init: false,
|
||||||
},
|
|
||||||
nazhua: {
|
nazhua: {
|
||||||
title: '哪吒监控',
|
title: '哪吒监控',
|
||||||
|
// 如果打包禁用 Sarasa Term SC 字体,默认为禁用该字体的配置
|
||||||
|
disableSarasaTermSC: import.meta.env.VITE_DISABLE_SARASA_TERM_SC === '1',
|
||||||
|
|
||||||
|
nezhaVersion: ['v0', 'v1'].includes(defaultNezhaVersion) ? defaultNezhaVersion : null,
|
||||||
apiMonitorPath: '/api/v1/monitor/{id}',
|
apiMonitorPath: '/api/v1/monitor/{id}',
|
||||||
wsPath: '/ws',
|
wsPath: '/ws',
|
||||||
nezhaPath: '/nezha/',
|
nezhaPath: '/nezha/',
|
||||||
nezhaV0ConfigType: 'servers',
|
nezhaV0ConfigType: 'servers',
|
||||||
|
v1ApiMonitorPath: '/api/v1/service/{id}',
|
||||||
|
v1WsPath: '/api/v1/ws/server',
|
||||||
|
v1ApiGroupPath: '/api/v1/server-group',
|
||||||
|
v1ApiSettingPath: '/api/v1/setting',
|
||||||
|
v1ApiProfilePath: '/api/v1/profile',
|
||||||
// 解构载入自定义配置
|
// 解构载入自定义配置
|
||||||
...(window.$$nazhuaConfig || {}),
|
...(window.$$nazhuaConfig || {}),
|
||||||
},
|
},
|
||||||
};
|
});
|
||||||
|
|
||||||
|
if (config.nazhua.nezhaVersion) {
|
||||||
|
config.init = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handle$$serverStatus() {
|
||||||
|
if (window.$$serverStatus) {
|
||||||
|
config.nazhua.listServerItemType = 'server-status';
|
||||||
|
config.nazhua.homeWorldMapPosition = 'bottom';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
handle$$serverStatus();
|
||||||
|
|
||||||
|
function setColorMode() {
|
||||||
|
if (config.nazhua.simpleColorMode) {
|
||||||
|
document.body.classList.add('simple-color-mode');
|
||||||
|
} else {
|
||||||
|
document.body.classList.remove('simple-color-mode');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setColorMode();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 替换网站图标
|
||||||
|
*/
|
||||||
|
function replaceFavicon() {
|
||||||
|
if (config.nazhua.customFavicon) {
|
||||||
|
const link = document.querySelector("link[rel*='icon']");
|
||||||
|
link.type = 'image/x-icon';
|
||||||
|
link.rel = 'shortcut icon';
|
||||||
|
link.href = config.nazhua.customFavicon;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
replaceFavicon();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 合并自定义配置
|
||||||
|
*/
|
||||||
export function mergeNazhuaConfig(customConfig) {
|
export function mergeNazhuaConfig(customConfig) {
|
||||||
Object.keys(customConfig).forEach((key) => {
|
Object.keys(customConfig).forEach((key) => {
|
||||||
config.nazhua[key] = customConfig[key];
|
config.nazhua[key] = customConfig[key];
|
||||||
});
|
});
|
||||||
|
replaceFavicon();
|
||||||
|
setColorMode();
|
||||||
|
handle$$serverStatus();
|
||||||
}
|
}
|
||||||
|
// 暴露合并配置方法
|
||||||
|
window.$mergeNazhuaConfig = mergeNazhuaConfig;
|
||||||
|
|
||||||
export default config;
|
export default config;
|
||||||
|
|
||||||
|
export const init = async () => {
|
||||||
|
await loadNezhaV1Profile(true).then((res) => {
|
||||||
|
config.nazhua.nezhaVersion = res ? 'v1' : 'v0';
|
||||||
|
});
|
||||||
|
config.init = true;
|
||||||
|
};
|
||||||
|
|||||||
@ -77,6 +77,12 @@ const codeMaps = {
|
|||||||
name: '吉隆坡',
|
name: '吉隆坡',
|
||||||
country: '马来西亚',
|
country: '马来西亚',
|
||||||
},
|
},
|
||||||
|
BKK: {
|
||||||
|
name: '曼谷',
|
||||||
|
country: '泰国',
|
||||||
|
x: 985,
|
||||||
|
y: 296,
|
||||||
|
},
|
||||||
HAN: {
|
HAN: {
|
||||||
x: 998,
|
x: 998,
|
||||||
y: 274,
|
y: 274,
|
||||||
@ -89,6 +95,24 @@ 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,
|
||||||
@ -120,8 +144,8 @@ const codeMaps = {
|
|||||||
country: '美国',
|
country: '美国',
|
||||||
},
|
},
|
||||||
MIA: {
|
MIA: {
|
||||||
x: 243,
|
x: 237,
|
||||||
y: 244,
|
y: 249,
|
||||||
name: '迈阿密',
|
name: '迈阿密',
|
||||||
country: '美国',
|
country: '美国',
|
||||||
},
|
},
|
||||||
@ -137,12 +161,54 @@ const codeMaps = {
|
|||||||
name: '纽约',
|
name: '纽约',
|
||||||
country: '美国',
|
country: '美国',
|
||||||
},
|
},
|
||||||
|
IAD: {
|
||||||
|
name: '阿什本',
|
||||||
|
country: 'US',
|
||||||
|
x: 265,
|
||||||
|
y: 186,
|
||||||
|
},
|
||||||
|
DFW: {
|
||||||
|
x: 172,
|
||||||
|
y: 211,
|
||||||
|
name: '达拉斯',
|
||||||
|
country: '美国',
|
||||||
|
},
|
||||||
|
ATL: {
|
||||||
|
x: 225,
|
||||||
|
y: 205,
|
||||||
|
name: '亚特兰大',
|
||||||
|
country: '美国',
|
||||||
|
},
|
||||||
|
HNL: {
|
||||||
|
x: 28,
|
||||||
|
y: 270,
|
||||||
|
name: '檀香山',
|
||||||
|
country: '美国',
|
||||||
|
},
|
||||||
YYZ: {
|
YYZ: {
|
||||||
x: 267,
|
x: 267,
|
||||||
y: 161,
|
y: 161,
|
||||||
name: '多伦多',
|
name: '多伦多',
|
||||||
country: '加拿大',
|
country: '加拿大',
|
||||||
},
|
},
|
||||||
|
MEX: {
|
||||||
|
x: 158,
|
||||||
|
y: 280,
|
||||||
|
name: '墨西哥城',
|
||||||
|
country: '墨西哥',
|
||||||
|
},
|
||||||
|
SCQ: {
|
||||||
|
x: 289,
|
||||||
|
y: 513,
|
||||||
|
name: '圣地亚哥',
|
||||||
|
country: '智利',
|
||||||
|
},
|
||||||
|
GRU: {
|
||||||
|
x: 370,
|
||||||
|
y: 473,
|
||||||
|
name: '圣保罗',
|
||||||
|
country: '巴西',
|
||||||
|
},
|
||||||
SYD: {
|
SYD: {
|
||||||
x: 1167,
|
x: 1167,
|
||||||
y: 519,
|
y: 519,
|
||||||
@ -167,6 +233,12 @@ const codeMaps = {
|
|||||||
name: '法兰克福',
|
name: '法兰克福',
|
||||||
country: '德国',
|
country: '德国',
|
||||||
},
|
},
|
||||||
|
BER: {
|
||||||
|
x: 620,
|
||||||
|
y: 130,
|
||||||
|
name: '柏林',
|
||||||
|
country: '德国',
|
||||||
|
},
|
||||||
LUX: {
|
LUX: {
|
||||||
x: 591,
|
x: 591,
|
||||||
y: 140,
|
y: 140,
|
||||||
@ -179,6 +251,24 @@ const codeMaps = {
|
|||||||
name: '巴黎',
|
name: '巴黎',
|
||||||
country: '法国',
|
country: '法国',
|
||||||
},
|
},
|
||||||
|
WAW: {
|
||||||
|
name: '华沙',
|
||||||
|
country: '波兰',
|
||||||
|
x: 649,
|
||||||
|
y: 123,
|
||||||
|
},
|
||||||
|
MAD: {
|
||||||
|
name: '马德里',
|
||||||
|
country: '西班牙',
|
||||||
|
x: 554,
|
||||||
|
y: 180,
|
||||||
|
},
|
||||||
|
MXP: {
|
||||||
|
name: '米兰',
|
||||||
|
country: '意大利',
|
||||||
|
x: 604,
|
||||||
|
y: 153,
|
||||||
|
},
|
||||||
SVO: {
|
SVO: {
|
||||||
x: 704,
|
x: 704,
|
||||||
y: 115,
|
y: 115,
|
||||||
@ -191,6 +281,30 @@ 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,
|
||||||
@ -215,14 +329,19 @@ export const aliasMapping = {
|
|||||||
HK: 'HKG',
|
HK: 'HKG',
|
||||||
MO: 'MFM',
|
MO: 'MFM',
|
||||||
TW: 'TPE',
|
TW: 'TPE',
|
||||||
|
ASH: 'IAD',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const countryCodeMapping = {
|
export const countryCodeMapping = {
|
||||||
|
CN: 'PEK',
|
||||||
JP: 'TYO',
|
JP: 'TYO',
|
||||||
SG: 'SIN',
|
SG: 'SIN',
|
||||||
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',
|
||||||
@ -234,6 +353,17 @@ export const countryCodeMapping = {
|
|||||||
GB: 'LON',
|
GB: 'LON',
|
||||||
AU: 'SYD',
|
AU: 'SYD',
|
||||||
US: 'LAX',
|
US: 'LAX',
|
||||||
|
CA: 'YYZ',
|
||||||
|
MX: 'MEX',
|
||||||
|
CL: 'SCQ',
|
||||||
|
BR: 'GRU',
|
||||||
|
IT: 'MXP',
|
||||||
|
ES: 'MAD',
|
||||||
|
PL: 'WAW',
|
||||||
|
BG: 'SOF',
|
||||||
|
LT: 'VNO',
|
||||||
|
NO: 'OSL',
|
||||||
|
MA: 'RBA',
|
||||||
};
|
};
|
||||||
|
|
||||||
export default codeMaps;
|
export default codeMaps;
|
||||||
|
|||||||
59
src/layout/components/dashboard-btn.vue
Normal file
59
src/layout/components/dashboard-btn.vue
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="nezha-user-info-group"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
:href="dashboardUrl"
|
||||||
|
class="dashboard-url"
|
||||||
|
:title="userLogin ? '访问管理后台' : '登录管理后台'"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
:class="{
|
||||||
|
'ri-dashboard-3-line': userLogin,
|
||||||
|
'ri-user-line': !userLogin,
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
<span>{{ userLogin ? '管理后台' : '登录' }}</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
/**
|
||||||
|
* 控制台入口
|
||||||
|
*/
|
||||||
|
import {
|
||||||
|
computed,
|
||||||
|
} from 'vue';
|
||||||
|
import {
|
||||||
|
useStore,
|
||||||
|
} from 'vuex';
|
||||||
|
|
||||||
|
import config from '@/config';
|
||||||
|
|
||||||
|
const store = useStore();
|
||||||
|
|
||||||
|
const userLogin = computed(() => store.state.profile?.username);
|
||||||
|
const dashboardUrl = computed(() => config.nazhua.v1DashboardUrl || '/dashboard');
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.nezha-user-info-group {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0 20px;
|
||||||
|
|
||||||
|
.dashboard-url {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0 5px;
|
||||||
|
color: #ddd;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #ff9a00;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -1,11 +1,18 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="layout-footer">
|
<div class="layout-footer">
|
||||||
|
<div
|
||||||
|
v-if="footerSlogan"
|
||||||
|
class="footer-slogan"
|
||||||
|
>
|
||||||
|
<div v-html="footerSlogan" />
|
||||||
|
</div>
|
||||||
<div class="copyright-text">
|
<div class="copyright-text">
|
||||||
<span class="text">
|
<span class="text">
|
||||||
Powered by
|
Powered by
|
||||||
<a
|
<a
|
||||||
ref="nofollow"
|
ref="nofollow"
|
||||||
href="https://nezha.wiki"
|
href="https://nezha.wiki"
|
||||||
|
:title="'当前为哪吒监控' + $config.nazhua.nezhaVersion"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>哪吒监控</a>
|
>哪吒监控</a>
|
||||||
</span>
|
</span>
|
||||||
@ -19,6 +26,10 @@
|
|||||||
{{ version }}
|
{{ version }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
ref="dynamicContentRef"
|
||||||
|
v-html="dynamicContent"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -27,7 +38,109 @@
|
|||||||
* Footer
|
* Footer
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
ref,
|
||||||
|
computed,
|
||||||
|
watch,
|
||||||
|
onMounted,
|
||||||
|
nextTick,
|
||||||
|
} from 'vue';
|
||||||
|
import { useStore } from 'vuex';
|
||||||
|
import config from '@/config';
|
||||||
|
|
||||||
const version = import.meta.env.VITE_APP_VERSION;
|
const version = import.meta.env.VITE_APP_VERSION;
|
||||||
|
const store = useStore();
|
||||||
|
|
||||||
|
const footerSlogan = computed(() => decodeURIComponent(config.nazhua?.footerSlogan || ''));
|
||||||
|
|
||||||
|
const dynamicContentRef = ref();
|
||||||
|
const executedScripts = ref(new Set()); // 记录已执行的脚本,避免重复执行
|
||||||
|
|
||||||
|
const dynamicContent = computed(() => {
|
||||||
|
if (store.state.setting?.config?.custom_code) {
|
||||||
|
return store.state.setting.config.custom_code;
|
||||||
|
}
|
||||||
|
if (store.state.setting?.custom_code) {
|
||||||
|
return store.state.setting.custom_code;
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
});
|
||||||
|
|
||||||
|
// 执行动态脚本的方法
|
||||||
|
const executeScripts = () => {
|
||||||
|
nextTick(() => {
|
||||||
|
if (!dynamicContentRef.value) return;
|
||||||
|
|
||||||
|
const scripts = dynamicContentRef.value.querySelectorAll('script');
|
||||||
|
|
||||||
|
scripts.forEach((script) => {
|
||||||
|
try {
|
||||||
|
// 生成脚本唯一标识,避免重复执行
|
||||||
|
const scriptIdentifier = script.src || script.textContent || '';
|
||||||
|
if (!scriptIdentifier || executedScripts.value.has(scriptIdentifier)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 清理已执行脚本的记录(当内容变化时)
|
||||||
|
const cleanupScripts = () => {
|
||||||
|
executedScripts.value.clear();
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(dynamicContent, (newVal, oldVal) => {
|
||||||
|
// 内容变化时,清理旧的执行记录
|
||||||
|
if (newVal !== oldVal) {
|
||||||
|
cleanupScripts();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newVal) {
|
||||||
|
// 确保 DOM 已更新
|
||||||
|
nextTick(() => {
|
||||||
|
executeScripts();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (dynamicContent.value) {
|
||||||
|
executeScripts();
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@ -36,6 +149,16 @@ const version = import.meta.env.VITE_APP_VERSION;
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #ccc;
|
color: #ccc;
|
||||||
|
|
||||||
|
.footer-slogan {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
|
||||||
|
font-size: 14px;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
.copyright-text {
|
.copyright-text {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|||||||
@ -12,91 +12,15 @@
|
|||||||
>{{ title }}</span>
|
>{{ title }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="right-box">
|
<div class="right-box">
|
||||||
<div
|
<server-count
|
||||||
v-if="serverCount?.total && showServerCount"
|
v-if="showServerCount"
|
||||||
class="server-count-group"
|
/>
|
||||||
>
|
<server-stat
|
||||||
<span class="server-count server-count--total">
|
v-if="showServerStat"
|
||||||
<span class="text">共</span>
|
/>
|
||||||
<span class="value">{{ serverCount.total }}</span>
|
<dashboard-btn
|
||||||
<span class="text">台服务器</span>
|
v-if="showDashboardBtn"
|
||||||
</span>
|
/>
|
||||||
<template v-if="serverCount.online !== serverCount.total">
|
|
||||||
<span
|
|
||||||
class="server-count server-count--online"
|
|
||||||
>
|
|
||||||
<span class="text">在线</span>
|
|
||||||
<span class="value">{{ serverCount.online }}</span>
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
class="server-count server-count--offline"
|
|
||||||
>
|
|
||||||
<span class="text">离线</span>
|
|
||||||
<span class="value">{{ serverCount.offline }}</span>
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-if="serverStat && showServerStat"
|
|
||||||
class="server-stat-group"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
v-if="serverStat.transfer"
|
|
||||||
class="server-stat server-stat--transfer"
|
|
||||||
>
|
|
||||||
<span class="server-stat-label">
|
|
||||||
<span class="text">流量</span>
|
|
||||||
</span>
|
|
||||||
<div class="server-stat-content">
|
|
||||||
<span class="server-stat-item server-stat-item--in">
|
|
||||||
<span class="ri-download-line" />
|
|
||||||
<span class="text-value">
|
|
||||||
{{ serverStat.transfer.inData.value }}
|
|
||||||
</span>
|
|
||||||
<span class="text-unit">
|
|
||||||
{{ serverStat.transfer.inData.unit }}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
<span class="server-stat-item server-stat-item--out">
|
|
||||||
<span class="ri-upload-line" />
|
|
||||||
<span class="text-value">
|
|
||||||
{{ serverStat.transfer.outData.value }}
|
|
||||||
</span>
|
|
||||||
<span class="text-unit">
|
|
||||||
{{ serverStat.transfer.outData.unit }}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-if="serverStat.netSpeed"
|
|
||||||
class="server-stat server-stat--net-speed"
|
|
||||||
>
|
|
||||||
<span class="server-stat-label">
|
|
||||||
<span class="text">网速</span>
|
|
||||||
</span>
|
|
||||||
<div class="server-stat-content">
|
|
||||||
<span class="server-stat-item server-stat-item--in">
|
|
||||||
<span class="ri-arrow-down-line" />
|
|
||||||
<span class="text-value">
|
|
||||||
{{ serverStat.netSpeed.inData.value }}
|
|
||||||
</span>
|
|
||||||
<span class="text-unit">
|
|
||||||
{{ serverStat.netSpeed.inData.unit }}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
<span class="server-stat-item server-stat-item--out">
|
|
||||||
<span class="ri-arrow-up-line" />
|
|
||||||
<span class="text-value">
|
|
||||||
{{ serverStat.netSpeed.outData.value }}
|
|
||||||
</span>
|
|
||||||
<span class="text-unit">
|
|
||||||
{{ serverStat.netSpeed.outData.unit }}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -107,25 +31,24 @@
|
|||||||
* LayoutHeader
|
* LayoutHeader
|
||||||
*/
|
*/
|
||||||
import {
|
import {
|
||||||
ref,
|
|
||||||
computed,
|
computed,
|
||||||
onMounted,
|
|
||||||
} from 'vue';
|
} from 'vue';
|
||||||
import {
|
|
||||||
useStore,
|
|
||||||
} from 'vuex';
|
|
||||||
import {
|
import {
|
||||||
useRoute,
|
useRoute,
|
||||||
useRouter,
|
useRouter,
|
||||||
} from 'vue-router';
|
} from 'vue-router';
|
||||||
import * as hostUtils from '@/utils/host';
|
|
||||||
|
|
||||||
import config from '@/config';
|
import config from '@/config';
|
||||||
|
|
||||||
const store = useStore();
|
import ServerCount from './server-count.vue';
|
||||||
|
import ServerStat from './server-stat.vue';
|
||||||
|
import DashboardBtn from './dashboard-btn.vue';
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
|
const lightBackground = computed(() => config.nazhua.lightBackground);
|
||||||
|
|
||||||
const headerStyle = computed(() => {
|
const headerStyle = computed(() => {
|
||||||
const style = {};
|
const style = {};
|
||||||
if (route.name === 'ServerDetail') {
|
if (route.name === 'ServerDetail') {
|
||||||
@ -136,120 +59,26 @@ const headerStyle = computed(() => {
|
|||||||
return style;
|
return style;
|
||||||
});
|
});
|
||||||
|
|
||||||
const showServerCount = config.nazhua.hideNavbarServerCount !== true;
|
const showServerCount = computed(() => config.nazhua.hideNavbarServerCount !== true);
|
||||||
const serverCount = computed(() => store.state.serverCount);
|
|
||||||
|
|
||||||
const showServerStat = config.nazhua.hideNavbarServerStat !== true;
|
const showServerStat = computed(() => config.nazhua.hideNavbarServerStat !== true);
|
||||||
const serverStat = computed(() => {
|
|
||||||
const transfer = {
|
|
||||||
in: 0,
|
|
||||||
inData: {
|
|
||||||
value: 0,
|
|
||||||
unit: '',
|
|
||||||
},
|
|
||||||
out: 0,
|
|
||||||
outData: {
|
|
||||||
value: 0,
|
|
||||||
unit: '',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const netSpeed = {
|
|
||||||
in: 0,
|
|
||||||
inData: {
|
|
||||||
value: 0,
|
|
||||||
unit: '',
|
|
||||||
},
|
|
||||||
out: 0,
|
|
||||||
outData: {
|
|
||||||
value: 0,
|
|
||||||
unit: '',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
if (store.state.serverList.length) {
|
|
||||||
store.state.serverList.forEach((server) => {
|
|
||||||
if (server.online === 1 && server.State) {
|
|
||||||
transfer.in += server.State.NetInTransfer;
|
|
||||||
transfer.out += server.State.NetOutTransfer;
|
|
||||||
netSpeed.in += server.State.NetInSpeed;
|
|
||||||
netSpeed.out += server.State.NetOutSpeed;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const calcInTransfer = hostUtils.calcBinary(transfer.in);
|
|
||||||
if (calcInTransfer.t > 1) {
|
|
||||||
transfer.inData.value = (calcInTransfer.t).toFixed(1) * 1;
|
|
||||||
transfer.inData.unit = 'T';
|
|
||||||
} else if (calcInTransfer.g > 1) {
|
|
||||||
transfer.inData.value = (calcInTransfer.g).toFixed(1) * 1;
|
|
||||||
transfer.inData.unit = 'G';
|
|
||||||
} else if (calcInTransfer.m > 1) {
|
|
||||||
transfer.inData.value = (calcInTransfer.m).toFixed(1) * 1;
|
|
||||||
transfer.inData.unit = 'M';
|
|
||||||
} else {
|
|
||||||
transfer.inData.value = calcInTransfer.value;
|
|
||||||
transfer.inData.unit = 'K';
|
|
||||||
}
|
|
||||||
const calcOutTransfer = hostUtils.calcBinary(transfer.out);
|
|
||||||
if (calcOutTransfer.t > 1) {
|
|
||||||
transfer.outData.value = (calcOutTransfer.t).toFixed(1) * 1;
|
|
||||||
transfer.outData.unit = 'T';
|
|
||||||
} else if (calcOutTransfer.g > 1) {
|
|
||||||
transfer.outData.value = (calcOutTransfer.g).toFixed(1) * 1;
|
|
||||||
transfer.outData.unit = 'G';
|
|
||||||
} else if (calcOutTransfer.m > 1) {
|
|
||||||
transfer.outData.value = (calcOutTransfer.m).toFixed(1) * 1;
|
|
||||||
transfer.outData.unit = 'M';
|
|
||||||
} else {
|
|
||||||
transfer.outData.value = calcOutTransfer.value;
|
|
||||||
transfer.outData.unit = 'K';
|
|
||||||
}
|
|
||||||
const calcNetInSpeed = hostUtils.calcBinary(netSpeed.in);
|
|
||||||
if (calcNetInSpeed.t > 1) {
|
|
||||||
netSpeed.inData.value = (calcNetInSpeed.t).toFixed(1) * 1;
|
|
||||||
netSpeed.inData.unit = 'T';
|
|
||||||
} else if (calcNetInSpeed.g > 1) {
|
|
||||||
netSpeed.inData.value = (calcNetInSpeed.g).toFixed(1) * 1;
|
|
||||||
netSpeed.inData.unit = 'G';
|
|
||||||
} else if (calcNetInSpeed.m > 1) {
|
|
||||||
netSpeed.inData.value = (calcNetInSpeed.m).toFixed(1) * 1;
|
|
||||||
netSpeed.inData.unit = 'M';
|
|
||||||
} else {
|
|
||||||
netSpeed.inData.value = (calcNetInSpeed.k).toFixed(1) * 1;
|
|
||||||
netSpeed.inData.unit = 'K';
|
|
||||||
}
|
|
||||||
const calcNetOutSpeed = hostUtils.calcBinary(netSpeed.out);
|
|
||||||
if (calcNetOutSpeed.t > 1) {
|
|
||||||
netSpeed.outData.value = (calcNetOutSpeed.t).toFixed(1) * 1;
|
|
||||||
netSpeed.outData.unit = 'T';
|
|
||||||
} else if (calcNetOutSpeed.g > 1) {
|
|
||||||
netSpeed.outData.value = (calcNetOutSpeed.g).toFixed(1) * 1;
|
|
||||||
netSpeed.outData.unit = 'G';
|
|
||||||
} else if (calcNetOutSpeed.m > 1) {
|
|
||||||
netSpeed.outData.value = (calcNetOutSpeed.m).toFixed(1) * 1;
|
|
||||||
netSpeed.outData.unit = 'M';
|
|
||||||
} else {
|
|
||||||
netSpeed.outData.value = (calcNetOutSpeed.k).toFixed(1) * 1;
|
|
||||||
netSpeed.outData.unit = 'K';
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
transfer,
|
|
||||||
netSpeed,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const title = ref(config.nazhua.title);
|
const title = computed(() => config.nazhua.title);
|
||||||
|
|
||||||
const headerClass = computed(() => {
|
const headerClass = computed(() => {
|
||||||
const classes = [];
|
const classes = [];
|
||||||
if (route.name === 'ServerDetail') {
|
if (route.name === 'ServerDetail') {
|
||||||
classes.push('layout-header--detail');
|
classes.push('layout-header--detail');
|
||||||
}
|
}
|
||||||
if (showServerStat) {
|
if (showServerStat.value) {
|
||||||
classes.push('layout-header--show-server-stat');
|
classes.push('layout-header--show-server-stat');
|
||||||
}
|
}
|
||||||
if (showServerCount) {
|
if (showServerCount.value) {
|
||||||
classes.push('layout-header--show-server-count');
|
classes.push('layout-header--show-server-count');
|
||||||
}
|
}
|
||||||
|
if (lightBackground.value) {
|
||||||
|
classes.push('layout-header--light-background');
|
||||||
|
}
|
||||||
return classes;
|
return classes;
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -261,9 +90,10 @@ function toHome() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
const showDashboardBtn = computed(() => [
|
||||||
title.value = config.nazhua.title;
|
config.nazhua.nezhaVersion === 'v1',
|
||||||
});
|
config.nazhua.v1HideNezhaDashboardBtn !== true,
|
||||||
|
].every((item) => item));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@ -284,43 +114,21 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&--light-background {
|
||||||
|
background-color: rgba(#000, 0.7);
|
||||||
|
background-image: none;
|
||||||
|
backdrop-filter: none;
|
||||||
|
}
|
||||||
|
|
||||||
.site-name {
|
.site-name {
|
||||||
line-height: calc(var(--layout-header-height) - 20px);
|
line-height: calc(var(--layout-header-height) - 20px);
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
|
font-weight: bold;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
text-shadow: 2px 2px 4px rgba(#000, 0.5);
|
text-shadow: 2px 2px 4px rgba(#000, 0.5);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.server-count-group {
|
|
||||||
display: flex;
|
|
||||||
gap: 10px;
|
|
||||||
|
|
||||||
.server-count {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 3px;
|
|
||||||
color: #ddd;
|
|
||||||
line-height: 30px;
|
|
||||||
|
|
||||||
&.server-count--total {
|
|
||||||
.value {
|
|
||||||
color: #70f3ff;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
&.server-count--online {
|
|
||||||
.value {
|
|
||||||
color: #0f0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
&.server-count--offline {
|
|
||||||
.value {
|
|
||||||
color: #f00;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.layer-header-container {
|
.layer-header-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
@ -340,73 +148,5 @@ onMounted(() => {
|
|||||||
gap: 0 20px;
|
gap: 0 20px;
|
||||||
color: #ddd;
|
color: #ddd;
|
||||||
}
|
}
|
||||||
|
|
||||||
.server-stat-group {
|
|
||||||
min-width: 160px;
|
|
||||||
|
|
||||||
@media screen and (max-width: 450px) {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
right: 0;
|
|
||||||
left: 0;
|
|
||||||
height: 28px;
|
|
||||||
padding: 0 20px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
flex-direction: row-reverse;
|
|
||||||
gap: 10px;
|
|
||||||
|
|
||||||
.server-stat-label {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.server-stat-content {
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.server-stat {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
line-height: 16px;
|
|
||||||
font-size: 12px;
|
|
||||||
|
|
||||||
.server-stat-content {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.server-stat-item {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.server-stat--transfer {
|
|
||||||
.server-stat-item--in {
|
|
||||||
.text-value {
|
|
||||||
color: var(--transfer-in-color);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.server-stat-item--out {
|
|
||||||
.text-value {
|
|
||||||
color: var(--transfer-out-color);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.server-stat--net-speed {
|
|
||||||
.server-stat-item--in {
|
|
||||||
.text-value {
|
|
||||||
color: var(--net-speed-in-color);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.server-stat-item--out {
|
|
||||||
.text-value {
|
|
||||||
color: var(--net-speed-out-color);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
332
src/layout/components/search-box.vue
Normal file
332
src/layout/components/search-box.vue
Normal file
@ -0,0 +1,332 @@
|
|||||||
|
<template>
|
||||||
|
<transition name="fadeIn">
|
||||||
|
<div
|
||||||
|
v-if="show"
|
||||||
|
class="search-box-background"
|
||||||
|
@click="closeSearchBox"
|
||||||
|
/>
|
||||||
|
</transition>
|
||||||
|
<transition name="fadeIn">
|
||||||
|
<div
|
||||||
|
v-if="show"
|
||||||
|
class="search-box-group"
|
||||||
|
>
|
||||||
|
<div class="search-box">
|
||||||
|
<input
|
||||||
|
ref="searchInputRef"
|
||||||
|
v-model.trim="searchWord"
|
||||||
|
type="text"
|
||||||
|
placeholder="可搜索服务器名称、标签、系统、国别代码"
|
||||||
|
class="search-box-input"
|
||||||
|
@input="onSearchInput"
|
||||||
|
@keydown.enter="onSearchInput"
|
||||||
|
@blur="onSearchInput"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
v-if="searchWord"
|
||||||
|
class="clear-btn"
|
||||||
|
@click="clearSearchWord"
|
||||||
|
>
|
||||||
|
<i class="clear-icon ri-close-fill" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="result-server-list-container">
|
||||||
|
<div class="search-list">
|
||||||
|
<search-list-item
|
||||||
|
v-for="item in searchResult"
|
||||||
|
:key="item.ID"
|
||||||
|
:info="item"
|
||||||
|
@open-detail="openDetail"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="search-active-btn"
|
||||||
|
@click="activeSearchBox"
|
||||||
|
>
|
||||||
|
<span class="icon">
|
||||||
|
<i class="ri-search-eye-line" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
/**
|
||||||
|
* 搜索盒子
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
computed,
|
||||||
|
ref,
|
||||||
|
onMounted,
|
||||||
|
onUnmounted,
|
||||||
|
} from 'vue';
|
||||||
|
import {
|
||||||
|
useStore,
|
||||||
|
} from 'vuex';
|
||||||
|
import {
|
||||||
|
useRouter,
|
||||||
|
} from 'vue-router';
|
||||||
|
|
||||||
|
import SearchListItem from './search-list-item.vue';
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const store = useStore();
|
||||||
|
const serverList = computed(() => store.state.serverList);
|
||||||
|
|
||||||
|
const show = ref(false);
|
||||||
|
const searchWord = ref('');
|
||||||
|
const searchResult = ref([]);
|
||||||
|
const searchInputRef = ref(null);
|
||||||
|
|
||||||
|
let handleSearchTimer = null;
|
||||||
|
function handleSearch() {
|
||||||
|
if (handleSearchTimer) {
|
||||||
|
clearTimeout(handleSearchTimer);
|
||||||
|
}
|
||||||
|
if (!searchWord.value) {
|
||||||
|
searchResult.value = [...serverList.value];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
handleSearchTimer = setTimeout(() => {
|
||||||
|
handleSearchTimer = null;
|
||||||
|
searchResult.value = serverList.value.filter((item) => {
|
||||||
|
{
|
||||||
|
const matched = item.Name.toLowerCase().includes(searchWord.value.toLowerCase());
|
||||||
|
if (matched) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (item?.PublicNote?.planDataMod) {
|
||||||
|
const {
|
||||||
|
networkRoute = '',
|
||||||
|
extra = '',
|
||||||
|
} = item.PublicNote.planDataMod;
|
||||||
|
return [
|
||||||
|
networkRoute.toLowerCase().includes(searchWord.value.toLowerCase()),
|
||||||
|
extra.toLowerCase().includes(searchWord.value.toLowerCase()),
|
||||||
|
(item.Host.Platform || '').toLowerCase().includes(searchWord.value.toLowerCase()),
|
||||||
|
(item.Host.CountryCode || '').toLowerCase().includes(searchWord.value.toLowerCase()),
|
||||||
|
].some((match) => match);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
}, 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSearchInput() {
|
||||||
|
handleSearch();
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearSearchWord() {
|
||||||
|
searchWord.value = '';
|
||||||
|
searchResult.value = [...serverList.value];
|
||||||
|
}
|
||||||
|
|
||||||
|
function activeSearchBox() {
|
||||||
|
searchWord.value = '';
|
||||||
|
searchResult.value = [...serverList.value];
|
||||||
|
show.value = true;
|
||||||
|
// 锁定页面滚动
|
||||||
|
document.body.style.overflow = 'hidden';
|
||||||
|
|
||||||
|
// 聚焦到搜索框
|
||||||
|
setTimeout(() => {
|
||||||
|
searchInputRef.value.focus();
|
||||||
|
}, 30);
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeSearchBox() {
|
||||||
|
show.value = false;
|
||||||
|
document.body.style.overflow = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function openDetail(info) {
|
||||||
|
router.push({
|
||||||
|
name: 'ServerDetail',
|
||||||
|
params: {
|
||||||
|
serverId: info.ID,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
closeSearchBox();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleKeyDown(event) {
|
||||||
|
if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 'k') {
|
||||||
|
event.stopPropagation();
|
||||||
|
event.preventDefault();
|
||||||
|
if (show.value) {
|
||||||
|
closeSearchBox();
|
||||||
|
} else {
|
||||||
|
activeSearchBox();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleEscKey(event) {
|
||||||
|
if (!show.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (event.key === 'Escape') {
|
||||||
|
closeSearchBox();
|
||||||
|
event.stopPropagation();
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
// 监听按下快捷键 Ctrl+K 打开搜索框
|
||||||
|
window.addEventListener('keydown', handleKeyDown);
|
||||||
|
// 监听按下 Esc 关闭搜索框
|
||||||
|
window.addEventListener('keydown', handleEscKey);
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
window.removeEventListener('keydown', handleKeyDown);
|
||||||
|
window.removeEventListener('keydown', handleEscKey);
|
||||||
|
if (handleSearchTimer) {
|
||||||
|
clearTimeout(handleSearchTimer);
|
||||||
|
handleSearchTimer = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.search-box-background {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(0, 0, 0, 0.15);
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-box-group {
|
||||||
|
position: fixed;
|
||||||
|
left: 50%;
|
||||||
|
top: 150px;
|
||||||
|
z-index: 1010;
|
||||||
|
transform: translate(-50%, 0);
|
||||||
|
width: 600px;
|
||||||
|
padding: 30px;
|
||||||
|
border-radius: 12px;
|
||||||
|
background-color: rgba(#000, 0.9);
|
||||||
|
|
||||||
|
@media screen and (max-width: 640px) {
|
||||||
|
width: auto;
|
||||||
|
top: 100px;
|
||||||
|
left: 20px;
|
||||||
|
right: 20px;
|
||||||
|
padding: 20px;
|
||||||
|
transform: translate(0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-box {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
padding-right: 40px;
|
||||||
|
border-radius: 20px;
|
||||||
|
background: #eee;
|
||||||
|
|
||||||
|
.search-box-input {
|
||||||
|
width: 100%;
|
||||||
|
height: 40px;
|
||||||
|
padding: 0 15px;
|
||||||
|
color: #234;
|
||||||
|
font-size: 14px;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
transition: 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clear-btn {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
line-height: 40px;
|
||||||
|
text-align: center;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: 0.3s;
|
||||||
|
|
||||||
|
.clear-icon {
|
||||||
|
font-size: 20px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-list {
|
||||||
|
margin-top: 10px;
|
||||||
|
height: 300px;
|
||||||
|
overflow-x: hidden;
|
||||||
|
overflow-y: auto;
|
||||||
|
@media screen and (max-width: 640px) {
|
||||||
|
height: 50vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-track {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-thumb {
|
||||||
|
background: rgba(255, 255, 255, 0.3);
|
||||||
|
border-radius: 4px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-active-btn {
|
||||||
|
position: fixed;
|
||||||
|
right: 20px;
|
||||||
|
bottom: 20px;
|
||||||
|
z-index: 10;
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba(#000, 0.7);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: 0.3s;
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
line-height: 1;
|
||||||
|
font-size: 24px;
|
||||||
|
color: #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(#000, 0.9);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.fadeIn-enter-active,
|
||||||
|
.fadeIn-leave-active {
|
||||||
|
transition: opacity 0.3s ease-in-out;
|
||||||
|
}
|
||||||
|
.fadeIn-enter-from,
|
||||||
|
.fadeIn-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
104
src/layout/components/search-list-item.vue
Normal file
104
src/layout/components/search-list-item.vue
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="search-list-item"
|
||||||
|
@click="openDetail"
|
||||||
|
>
|
||||||
|
<div class="server-name">
|
||||||
|
{{ info.Name }}
|
||||||
|
</div>
|
||||||
|
<div class="server-tag-list">
|
||||||
|
<span
|
||||||
|
v-for="(tagItem, index) in tagList"
|
||||||
|
:key="`${tagItem}_${index}`"
|
||||||
|
class="tag-item"
|
||||||
|
:class="{
|
||||||
|
'has-sarasa-term': $hasSarasaTerm && config.nazhua.disableSarasaTermSC !== true,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
{{ tagItem }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
/**
|
||||||
|
* 搜索后的单条展示
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
computed,
|
||||||
|
} from 'vue';
|
||||||
|
|
||||||
|
import config from '@/config';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
info: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emits = defineEmits([
|
||||||
|
'open-detail',
|
||||||
|
]);
|
||||||
|
|
||||||
|
const tagList = computed(() => {
|
||||||
|
const list = [];
|
||||||
|
const {
|
||||||
|
networkRoute,
|
||||||
|
extra,
|
||||||
|
} = props?.info?.PublicNote?.planDataMod || {};
|
||||||
|
if (networkRoute) {
|
||||||
|
list.push(...networkRoute.split(','));
|
||||||
|
}
|
||||||
|
if (extra) {
|
||||||
|
list.push(...extra.split(','));
|
||||||
|
}
|
||||||
|
// 列表最多显示3个标签
|
||||||
|
return list.slice(0, 3);
|
||||||
|
});
|
||||||
|
|
||||||
|
function openDetail() {
|
||||||
|
emits('open-detail', props.info);
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.search-list-item {
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
padding: 8px 10px;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
.server-name {
|
||||||
|
flex: 1;
|
||||||
|
line-height: 30px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.server-tag-list {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
height: 30px;
|
||||||
|
|
||||||
|
.tag-item {
|
||||||
|
height: 18px;
|
||||||
|
padding: 0 4px;
|
||||||
|
line-height: 18px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--public-note-tag-color);
|
||||||
|
background: var(--public-note-tag-bg);
|
||||||
|
text-shadow: 1px 1px 2px rgba(#000, 0.2);
|
||||||
|
border-radius: 4px;
|
||||||
|
|
||||||
|
&.has-sarasa-term {
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
79
src/layout/components/server-count.vue
Normal file
79
src/layout/components/server-count.vue
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
v-if="serverCount?.total"
|
||||||
|
class="server-count-group"
|
||||||
|
>
|
||||||
|
<span class="server-count server-count--total">
|
||||||
|
<span class="text">共</span>
|
||||||
|
<span class="value">{{ serverCount.total }}</span>
|
||||||
|
<span class="text">台服务器</span>
|
||||||
|
</span>
|
||||||
|
<template v-if="serverCount.online !== serverCount.total">
|
||||||
|
<span
|
||||||
|
class="server-count server-count--online"
|
||||||
|
>
|
||||||
|
<span class="text">在线</span>
|
||||||
|
<span class="value">{{ serverCount.online }}</span>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class="server-count server-count--offline"
|
||||||
|
>
|
||||||
|
<span class="text">离线</span>
|
||||||
|
<span class="value">{{ serverCount.offline }}</span>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
/**
|
||||||
|
* 服务器数量
|
||||||
|
*/
|
||||||
|
import {
|
||||||
|
computed,
|
||||||
|
} from 'vue';
|
||||||
|
import {
|
||||||
|
useStore,
|
||||||
|
} from 'vuex';
|
||||||
|
|
||||||
|
const store = useStore();
|
||||||
|
|
||||||
|
const serverCount = computed(() => store.state.serverCount);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.server-count-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
|
||||||
|
.server-count {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 3px;
|
||||||
|
color: #ddd;
|
||||||
|
line-height: 30px;
|
||||||
|
|
||||||
|
.value {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.server-count--total {
|
||||||
|
.value {
|
||||||
|
color: #70f3ff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.server-count--online {
|
||||||
|
.value {
|
||||||
|
color: #0f0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.server-count--offline {
|
||||||
|
.value {
|
||||||
|
color: #f00;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
254
src/layout/components/server-stat.vue
Normal file
254
src/layout/components/server-stat.vue
Normal file
@ -0,0 +1,254 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
v-if="serverStat"
|
||||||
|
class="server-stat-group"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="serverStat.transfer"
|
||||||
|
class="server-stat server-stat--transfer"
|
||||||
|
>
|
||||||
|
<span class="server-stat-label">
|
||||||
|
<span class="text">流量</span>
|
||||||
|
</span>
|
||||||
|
<div class="server-stat-content">
|
||||||
|
<span class="server-stat-item server-stat-item--in">
|
||||||
|
<span class="ri-download-line" />
|
||||||
|
<span class="text-value">
|
||||||
|
{{ serverStat.transfer.inData.value }}
|
||||||
|
</span>
|
||||||
|
<span class="text-unit">
|
||||||
|
{{ serverStat.transfer.inData.unit }}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span class="server-stat-item server-stat-item--out">
|
||||||
|
<span class="ri-upload-line" />
|
||||||
|
<span class="text-value">
|
||||||
|
{{ serverStat.transfer.outData.value }}
|
||||||
|
</span>
|
||||||
|
<span class="text-unit">
|
||||||
|
{{ serverStat.transfer.outData.unit }}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="serverStat.netSpeed"
|
||||||
|
class="server-stat server-stat--net-speed"
|
||||||
|
>
|
||||||
|
<span class="server-stat-label">
|
||||||
|
<span class="text">网速</span>
|
||||||
|
</span>
|
||||||
|
<div class="server-stat-content">
|
||||||
|
<span class="server-stat-item server-stat-item--in">
|
||||||
|
<span class="ri-arrow-down-line" />
|
||||||
|
<span class="text-value">
|
||||||
|
{{ serverStat.netSpeed.inData.value }}
|
||||||
|
</span>
|
||||||
|
<span class="text-unit">
|
||||||
|
{{ serverStat.netSpeed.inData.unit }}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span class="server-stat-item server-stat-item--out">
|
||||||
|
<span class="ri-arrow-up-line" />
|
||||||
|
<span class="text-value">
|
||||||
|
{{ serverStat.netSpeed.outData.value }}
|
||||||
|
</span>
|
||||||
|
<span class="text-unit">
|
||||||
|
{{ serverStat.netSpeed.outData.unit }}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
/**
|
||||||
|
* 服务器统计
|
||||||
|
*/
|
||||||
|
import {
|
||||||
|
computed,
|
||||||
|
} from 'vue';
|
||||||
|
import {
|
||||||
|
useStore,
|
||||||
|
} from 'vuex';
|
||||||
|
import * as hostUtils from '@/utils/host';
|
||||||
|
|
||||||
|
const store = useStore();
|
||||||
|
|
||||||
|
const serverStat = computed(() => {
|
||||||
|
const transfer = {
|
||||||
|
in: 0,
|
||||||
|
inData: {
|
||||||
|
value: 0,
|
||||||
|
unit: '',
|
||||||
|
},
|
||||||
|
out: 0,
|
||||||
|
outData: {
|
||||||
|
value: 0,
|
||||||
|
unit: '',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const netSpeed = {
|
||||||
|
in: 0,
|
||||||
|
inData: {
|
||||||
|
value: 0,
|
||||||
|
unit: '',
|
||||||
|
},
|
||||||
|
out: 0,
|
||||||
|
outData: {
|
||||||
|
value: 0,
|
||||||
|
unit: '',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
if (store.state.serverList.length) {
|
||||||
|
store.state.serverList.forEach((server) => {
|
||||||
|
if (server.online === 1 && server.State) {
|
||||||
|
if (typeof server.State.NetInTransfer === 'number') {
|
||||||
|
transfer.in += server.State.NetInTransfer;
|
||||||
|
}
|
||||||
|
if (typeof server.State.NetOutTransfer === 'number') {
|
||||||
|
transfer.out += server.State.NetOutTransfer;
|
||||||
|
}
|
||||||
|
if (typeof server.State.NetInSpeed === 'number') {
|
||||||
|
netSpeed.in += server.State.NetInSpeed;
|
||||||
|
}
|
||||||
|
if (typeof server.State.NetOutSpeed === 'number') {
|
||||||
|
netSpeed.out += server.State.NetOutSpeed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const calcInTransfer = hostUtils.calcBinary(transfer.in);
|
||||||
|
if (calcInTransfer.t > 1) {
|
||||||
|
transfer.inData.value = (calcInTransfer.t).toFixed(1) * 1;
|
||||||
|
transfer.inData.unit = 'T';
|
||||||
|
} else if (calcInTransfer.g > 1) {
|
||||||
|
transfer.inData.value = (calcInTransfer.g).toFixed(1) * 1;
|
||||||
|
transfer.inData.unit = 'G';
|
||||||
|
} else if (calcInTransfer.m > 1) {
|
||||||
|
transfer.inData.value = (calcInTransfer.m).toFixed(1) * 1;
|
||||||
|
transfer.inData.unit = 'M';
|
||||||
|
} else {
|
||||||
|
transfer.inData.value = calcInTransfer.value;
|
||||||
|
transfer.inData.unit = 'K';
|
||||||
|
}
|
||||||
|
const calcOutTransfer = hostUtils.calcBinary(transfer.out);
|
||||||
|
if (calcOutTransfer.t > 1) {
|
||||||
|
transfer.outData.value = (calcOutTransfer.t).toFixed(1) * 1;
|
||||||
|
transfer.outData.unit = 'T';
|
||||||
|
} else if (calcOutTransfer.g > 1) {
|
||||||
|
transfer.outData.value = (calcOutTransfer.g).toFixed(1) * 1;
|
||||||
|
transfer.outData.unit = 'G';
|
||||||
|
} else if (calcOutTransfer.m > 1) {
|
||||||
|
transfer.outData.value = (calcOutTransfer.m).toFixed(1) * 1;
|
||||||
|
transfer.outData.unit = 'M';
|
||||||
|
} else {
|
||||||
|
transfer.outData.value = calcOutTransfer.value;
|
||||||
|
transfer.outData.unit = 'K';
|
||||||
|
}
|
||||||
|
const calcNetInSpeed = hostUtils.calcBinary(netSpeed.in);
|
||||||
|
if (calcNetInSpeed.t > 1) {
|
||||||
|
netSpeed.inData.value = (calcNetInSpeed.t).toFixed(1) * 1;
|
||||||
|
netSpeed.inData.unit = 'T';
|
||||||
|
} else if (calcNetInSpeed.g > 1) {
|
||||||
|
netSpeed.inData.value = (calcNetInSpeed.g).toFixed(1) * 1;
|
||||||
|
netSpeed.inData.unit = 'G';
|
||||||
|
} else if (calcNetInSpeed.m > 1) {
|
||||||
|
netSpeed.inData.value = (calcNetInSpeed.m).toFixed(1) * 1;
|
||||||
|
netSpeed.inData.unit = 'M';
|
||||||
|
} else {
|
||||||
|
netSpeed.inData.value = (calcNetInSpeed.k).toFixed(1) * 1;
|
||||||
|
netSpeed.inData.unit = 'K';
|
||||||
|
}
|
||||||
|
const calcNetOutSpeed = hostUtils.calcBinary(netSpeed.out);
|
||||||
|
if (calcNetOutSpeed.t > 1) {
|
||||||
|
netSpeed.outData.value = (calcNetOutSpeed.t).toFixed(1) * 1;
|
||||||
|
netSpeed.outData.unit = 'T';
|
||||||
|
} else if (calcNetOutSpeed.g > 1) {
|
||||||
|
netSpeed.outData.value = (calcNetOutSpeed.g).toFixed(1) * 1;
|
||||||
|
netSpeed.outData.unit = 'G';
|
||||||
|
} else if (calcNetOutSpeed.m > 1) {
|
||||||
|
netSpeed.outData.value = (calcNetOutSpeed.m).toFixed(1) * 1;
|
||||||
|
netSpeed.outData.unit = 'M';
|
||||||
|
} else {
|
||||||
|
netSpeed.outData.value = (calcNetOutSpeed.k).toFixed(1) * 1;
|
||||||
|
netSpeed.outData.unit = 'K';
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
transfer,
|
||||||
|
netSpeed,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.server-stat-group {
|
||||||
|
min-width: 160px;
|
||||||
|
|
||||||
|
@media screen and (max-width: 450px) {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
left: 0;
|
||||||
|
height: 28px;
|
||||||
|
padding: 0 20px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: row-reverse;
|
||||||
|
gap: 10px;
|
||||||
|
|
||||||
|
.server-stat-label {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.server-stat-content {
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.server-stat {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
line-height: 16px;
|
||||||
|
font-size: 12px;
|
||||||
|
|
||||||
|
.server-stat-content {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.server-stat-item {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.server-stat--transfer {
|
||||||
|
.server-stat-item--in {
|
||||||
|
.text-value {
|
||||||
|
color: var(--transfer-in-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.server-stat-item--out {
|
||||||
|
.text-value {
|
||||||
|
color: var(--transfer-out-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.server-stat--net-speed {
|
||||||
|
.server-stat-item--in {
|
||||||
|
.text-value {
|
||||||
|
color: var(--net-speed-in-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.server-stat-item--out {
|
||||||
|
.text-value {
|
||||||
|
color: var(--net-speed-out-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -1,11 +1,27 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="layout-group">
|
<div
|
||||||
<div class="layout-bg" />
|
class="layout-group"
|
||||||
|
:style="layoutGroupStyle"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="layout-bg"
|
||||||
|
:style="layoutBGStyle"
|
||||||
|
/>
|
||||||
<div class="layout-main">
|
<div class="layout-main">
|
||||||
<layout-header />
|
<layout-header />
|
||||||
<slot />
|
<slot />
|
||||||
<layout-footer />
|
<layout-footer />
|
||||||
|
|
||||||
|
<search-box
|
||||||
|
v-if="enableInnerSearch"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<template v-if="showFireworks">
|
||||||
|
<fireworks />
|
||||||
|
</template>
|
||||||
|
<template v-if="config.nazhua.showLantern">
|
||||||
|
<lantern />
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -13,8 +29,60 @@
|
|||||||
/**
|
/**
|
||||||
* LayoutMain
|
* LayoutMain
|
||||||
*/
|
*/
|
||||||
|
import {
|
||||||
|
ref,
|
||||||
|
computed,
|
||||||
|
onUnmounted,
|
||||||
|
} from 'vue';
|
||||||
|
import config from '@/config';
|
||||||
|
import Fireworks from '@/components/fireworks.vue';
|
||||||
|
import Lantern from '@/components/lantern.vue';
|
||||||
import LayoutHeader from './components/header.vue';
|
import LayoutHeader from './components/header.vue';
|
||||||
import LayoutFooter from './components/footer.vue';
|
import LayoutFooter from './components/footer.vue';
|
||||||
|
import SearchBox from './components/search-box.vue';
|
||||||
|
|
||||||
|
const windowWidth = ref(window.innerWidth);
|
||||||
|
|
||||||
|
const layoutGroupStyle = computed(() => {
|
||||||
|
const style = {};
|
||||||
|
if (config.nazhua.lightBackground) {
|
||||||
|
style['--layout-main-bg-color'] = 'rgba(20, 30, 40, 0.2)';
|
||||||
|
}
|
||||||
|
return style;
|
||||||
|
});
|
||||||
|
|
||||||
|
const layoutBGStyle = computed(() => {
|
||||||
|
const style = {};
|
||||||
|
if (config.nazhua.customBackgroundImage) {
|
||||||
|
style.background = `url(${config.nazhua.customBackgroundImage}) 50% 50%`;
|
||||||
|
style.backgroundSize = 'cover';
|
||||||
|
}
|
||||||
|
return style;
|
||||||
|
});
|
||||||
|
|
||||||
|
const showFireworks = computed(() => {
|
||||||
|
if (windowWidth.value < 800) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return config.nazhua.showFireworks;
|
||||||
|
});
|
||||||
|
|
||||||
|
const enableInnerSearch = computed(() => {
|
||||||
|
if (typeof config.nazhua.enableInnerSearch === 'undefined') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return config.nazhua.enableInnerSearch;
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleResize = () => {
|
||||||
|
windowWidth.value = window.innerWidth;
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('resize', handleResize);
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
window.removeEventListener('resize', handleResize);
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|||||||
51
src/load.js
Normal file
51
src/load.js
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
// 是否禁用 Sarasa Term SC 字体
|
||||||
|
if (import.meta.env.VITE_DISABLE_SARASA_TERM_SC !== '1') {
|
||||||
|
if (import.meta.env.VITE_SARASA_TERM_SC_USE_CDN) {
|
||||||
|
import('./assets/fonts/SarasaTermSC/cdn-font.css');
|
||||||
|
} else {
|
||||||
|
import('./assets/fonts/SarasaTermSC/font.css');
|
||||||
|
}
|
||||||
|
import('./assets/scss/sarasa-term-sc.scss');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使用 CDN 加载 CSS 文件
|
||||||
|
*/
|
||||||
|
function useCdnCss(item) {
|
||||||
|
const cdnType = import.meta.env.VITE_CDN_LIB_TYPE;
|
||||||
|
let cssUrl = item.jsdelivr;
|
||||||
|
if (['cdnjs', 'loli'].includes(cdnType)) {
|
||||||
|
cssUrl = item.cdnjs;
|
||||||
|
if (cdnType === 'loli') {
|
||||||
|
cssUrl = cssUrl.replace('https://cdnjs.cloudflare.com/', 'https://cdnjs.loli.net/');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const cdnStylesheet = document.createElement('link');
|
||||||
|
cdnStylesheet.rel = 'stylesheet';
|
||||||
|
cdnStylesheet.href = cssUrl;
|
||||||
|
document.head.appendChild(cdnStylesheet);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 判断是否使用 CDN
|
||||||
|
if (import.meta.env.VITE_USE_CDN) {
|
||||||
|
Object.entries({
|
||||||
|
remixicon: {
|
||||||
|
jsdelivr: 'https://cdn.jsdelivr.net/npm/remixicon@4.7.0/fonts/remixicon.css',
|
||||||
|
cdnjs: 'https://cdnjs.cloudflare.com/ajax/libs/remixicon/4.2.0/remixicon.css',
|
||||||
|
},
|
||||||
|
flagIcons: {
|
||||||
|
jsdelivr: 'https://cdn.jsdelivr.net/npm/flag-icons@7.2.3/css/flag-icons.min.css',
|
||||||
|
cdnjs: 'https://cdnjs.cloudflare.com/ajax/libs/flag-icons/7.2.3/css/flag-icons.min.css',
|
||||||
|
},
|
||||||
|
fontLogos: {
|
||||||
|
jsdelivr: 'https://cdn.jsdelivr.net/npm/font-logos@1.3.0/assets/font-logos.css',
|
||||||
|
cdnjs: 'https://cdnjs.cloudflare.com/ajax/libs/font-logos/1.2.0/font-logos.css',
|
||||||
|
},
|
||||||
|
}).forEach(([, item]) => {
|
||||||
|
useCdnCss(item);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
import('remixicon/fonts/remixicon.css');
|
||||||
|
import('flag-icons/css/flag-icons.min.css');
|
||||||
|
import('font-logos/assets/font-logos.css');
|
||||||
|
}
|
||||||
@ -4,6 +4,7 @@ import {
|
|||||||
createWebHashHistory,
|
createWebHashHistory,
|
||||||
} from 'vue-router';
|
} from 'vue-router';
|
||||||
import config from '@/config';
|
import config from '@/config';
|
||||||
|
import pageTitle from '@/utils/page-title';
|
||||||
|
|
||||||
const constantRoutes = [{
|
const constantRoutes = [{
|
||||||
name: 'Home',
|
name: 'Home',
|
||||||
@ -26,16 +27,25 @@ const constantRoutes = [{
|
|||||||
|
|
||||||
const routerOptions = {
|
const routerOptions = {
|
||||||
history: config.nazhua.routeMode === 'h5' ? createWebHistory() : createWebHashHistory(),
|
history: config.nazhua.routeMode === 'h5' ? createWebHistory() : createWebHashHistory(),
|
||||||
scrollBehavior: () => ({
|
scrollBehavior: (to, from, savedPosition) => {
|
||||||
|
if (savedPosition) {
|
||||||
|
return savedPosition;
|
||||||
|
}
|
||||||
|
return {
|
||||||
top: 0,
|
top: 0,
|
||||||
behavior: 'smooth',
|
behavior: 'smooth',
|
||||||
}),
|
};
|
||||||
|
},
|
||||||
routes: constantRoutes,
|
routes: constantRoutes,
|
||||||
};
|
};
|
||||||
const router = createRouter(routerOptions);
|
const router = createRouter(routerOptions);
|
||||||
|
|
||||||
router.beforeResolve((to, from, next) => {
|
router.beforeResolve((to, from, next) => {
|
||||||
document.title = [to?.meta?.title, config.nazhua.title].filter((i) => i).join(' - ');
|
if (to?.meta?.title) {
|
||||||
|
pageTitle(to?.meta?.title);
|
||||||
|
} else if (to.name === 'Home') {
|
||||||
|
pageTitle(config.nazhua.title);
|
||||||
|
}
|
||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -2,7 +2,15 @@ import {
|
|||||||
createStore,
|
createStore,
|
||||||
} from 'vuex';
|
} from 'vuex';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import loadNezhaConfig from '@/utils/load-nezha-config';
|
import config from '@/config';
|
||||||
|
import loadNezhaV0Config, {
|
||||||
|
loadServerGroup as loadNezhaV0ServerGroup,
|
||||||
|
} from '@/utils/load-nezha-v0-config';
|
||||||
|
import {
|
||||||
|
loadServerGroup as loadNezhaV1ServerGroup,
|
||||||
|
loadSetting as loadNezhaV1Setting,
|
||||||
|
loadProfile as loadNezhaV1Profile,
|
||||||
|
} from '@/utils/load-nezha-v1-config';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
msg,
|
msg,
|
||||||
@ -10,12 +18,17 @@ import {
|
|||||||
|
|
||||||
const defaultState = () => ({
|
const defaultState = () => ({
|
||||||
init: false,
|
init: false,
|
||||||
|
serverTime: 0,
|
||||||
|
serverGroup: [],
|
||||||
serverList: [],
|
serverList: [],
|
||||||
|
serverListColumnWidths: {},
|
||||||
serverCount: {
|
serverCount: {
|
||||||
total: 0,
|
total: 0,
|
||||||
online: 0,
|
online: 0,
|
||||||
offline: 0,
|
offline: 0,
|
||||||
},
|
},
|
||||||
|
profile: {},
|
||||||
|
setting: {},
|
||||||
});
|
});
|
||||||
|
|
||||||
function isOnline(LastActive, currentTime = Date.now()) {
|
function isOnline(LastActive, currentTime = Date.now()) {
|
||||||
@ -35,9 +48,16 @@ function handleServerCount(servers) {
|
|||||||
return counts;
|
return counts;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let firstSetServers = true;
|
||||||
const store = createStore({
|
const store = createStore({
|
||||||
state: defaultState(),
|
state: defaultState(),
|
||||||
mutations: {
|
mutations: {
|
||||||
|
SET_SERVER_TIME(state, time) {
|
||||||
|
state.serverTime = time;
|
||||||
|
},
|
||||||
|
SET_SERVER_GROUP(state, serverGroup) {
|
||||||
|
state.serverGroup = serverGroup;
|
||||||
|
},
|
||||||
SET_SERVERS(state, servers) {
|
SET_SERVERS(state, servers) {
|
||||||
const newServers = [...servers];
|
const newServers = [...servers];
|
||||||
newServers.sort((a, b) => b.DisplayIndex - a.DisplayIndex);
|
newServers.sort((a, b) => b.DisplayIndex - a.DisplayIndex);
|
||||||
@ -58,8 +78,6 @@ const store = createStore({
|
|||||||
};
|
};
|
||||||
if (oldItem?.PublicNote) {
|
if (oldItem?.PublicNote) {
|
||||||
serverItem.PublicNote = oldItem.PublicNote;
|
serverItem.PublicNote = oldItem.PublicNote;
|
||||||
} else {
|
|
||||||
serverItem.PublicNote = {};
|
|
||||||
}
|
}
|
||||||
return serverItem;
|
return serverItem;
|
||||||
});
|
});
|
||||||
@ -69,13 +87,54 @@ const store = createStore({
|
|||||||
state.serverCount = handleServerCount(newServers);
|
state.serverCount = handleServerCount(newServers);
|
||||||
state.init = true;
|
state.init = true;
|
||||||
},
|
},
|
||||||
|
SET_PROFILE(state, profile) {
|
||||||
|
state.profile = profile;
|
||||||
|
},
|
||||||
|
SET_SETTING(state, setting) {
|
||||||
|
state.setting = setting;
|
||||||
|
},
|
||||||
|
SET_SERVER_LIST_COLUMN_WIDTHS(state, widths) {
|
||||||
|
state.serverListColumnWidths = widths;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
/**
|
/**
|
||||||
* 加载服务器列表
|
* 加载服务器列表
|
||||||
*/
|
*/
|
||||||
async loadServers({ commit }) {
|
async initServerInfo({ commit }, params) {
|
||||||
const serverResult = await loadNezhaConfig();
|
firstSetServers = true;
|
||||||
|
// 如果是v1版本的话,加载v1版本的数据
|
||||||
|
if (config.nazhua.nezhaVersion === 'v1') {
|
||||||
|
const {
|
||||||
|
route,
|
||||||
|
} = params || {};
|
||||||
|
loadNezhaV1ServerGroup().then((res) => {
|
||||||
|
if (res) {
|
||||||
|
commit('SET_SERVER_GROUP', res);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
loadNezhaV1Setting().then((res) => {
|
||||||
|
if (res) {
|
||||||
|
commit('SET_SETTING', res);
|
||||||
|
// 如果自定义配置没有设置title,使用站点名称
|
||||||
|
if (!window.$$nazhuaConfig.title) {
|
||||||
|
config.nazhua.title = res.config?.site_name || res.site_name;
|
||||||
|
if (route?.name === 'Home' || !route) {
|
||||||
|
document.title = config.nazhua.title;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
loadNezhaV1Profile().then((res) => {
|
||||||
|
if (res) {
|
||||||
|
commit('SET_PROFILE', res);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 如果是v0版本的话,加载v0版本的数据
|
||||||
|
// 加载初始化的服务器列表,需要其中的公开备注字段
|
||||||
|
const serverResult = await loadNezhaV0Config();
|
||||||
if (!serverResult) {
|
if (!serverResult) {
|
||||||
console.error('load server config failed');
|
console.error('load server config failed');
|
||||||
return;
|
return;
|
||||||
@ -87,6 +146,11 @@ const store = createStore({
|
|||||||
};
|
};
|
||||||
return item;
|
return item;
|
||||||
}) || [];
|
}) || [];
|
||||||
|
const res = loadNezhaV0ServerGroup(servers);
|
||||||
|
if (res) {
|
||||||
|
commit('SET_SERVER_GROUP', res);
|
||||||
|
}
|
||||||
|
firstSetServers = false;
|
||||||
commit('SET_SERVERS', servers);
|
commit('SET_SERVERS', servers);
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
@ -97,6 +161,9 @@ const store = createStore({
|
|||||||
}) {
|
}) {
|
||||||
msg.on('servers', (res) => {
|
msg.on('servers', (res) => {
|
||||||
if (res) {
|
if (res) {
|
||||||
|
if (res.now) {
|
||||||
|
commit('SET_SERVER_TIME', res.now);
|
||||||
|
}
|
||||||
const servers = res.servers?.map?.((i) => {
|
const servers = res.servers?.map?.((i) => {
|
||||||
const item = {
|
const item = {
|
||||||
...i,
|
...i,
|
||||||
@ -104,10 +171,50 @@ const store = createStore({
|
|||||||
};
|
};
|
||||||
return item;
|
return item;
|
||||||
}) || [];
|
}) || [];
|
||||||
|
if (firstSetServers) {
|
||||||
|
firstSetServers = false;
|
||||||
|
commit('SET_SERVERS', servers);
|
||||||
|
|
||||||
|
// 在v0没抓页面配置的情况下,从服务器列表中分离出标签列表
|
||||||
|
if (config.nazhua.nezhaVersion !== 'v1') {
|
||||||
|
const group = loadNezhaV0ServerGroup(servers);
|
||||||
|
if (group) {
|
||||||
|
commit('SET_SERVER_GROUP', group);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
commit('UPDATE_SERVERS', servers);
|
commit('UPDATE_SERVERS', servers);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* 设置服务器列表行宽度
|
||||||
|
*/
|
||||||
|
setServerListColumnWidths({
|
||||||
|
commit,
|
||||||
|
state,
|
||||||
|
}, data) {
|
||||||
|
const newWidths = {
|
||||||
|
...state.serverListColumnWidths,
|
||||||
|
...data,
|
||||||
|
};
|
||||||
|
commit('SET_SERVER_LIST_COLUMN_WIDTHS', newWidths);
|
||||||
|
},
|
||||||
|
setServerListColumnWidth({
|
||||||
|
commit,
|
||||||
|
state,
|
||||||
|
}, data) {
|
||||||
|
const newWidths = {
|
||||||
|
...state.serverListColumnWidths,
|
||||||
|
};
|
||||||
|
if (newWidths[data.prop]) {
|
||||||
|
newWidths[data.prop] = Math.max(newWidths[data.prop], data.width);
|
||||||
|
} else {
|
||||||
|
newWidths[data.prop] = data.width;
|
||||||
|
}
|
||||||
|
commit('SET_SERVER_LIST_COLUMN_WIDTHS', newWidths);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
15
src/use.js
15
src/use.js
@ -1,11 +1,20 @@
|
|||||||
import 'remixicon/fonts/remixicon.css';
|
import './load';
|
||||||
import 'flag-icons/css/flag-icons.min.css';
|
|
||||||
import 'font-logos/assets/font-logos.css';
|
|
||||||
import './assets/scss/base.scss';
|
import './assets/scss/base.scss';
|
||||||
import router from './router';
|
import router from './router';
|
||||||
import store from './store';
|
import store from './store';
|
||||||
|
import config from './config';
|
||||||
|
|
||||||
|
import DotDotBox from './components/dot-dot-box.vue';
|
||||||
|
import Popover from './components/popover.vue';
|
||||||
|
import ServerFlag from './components/server-flag.vue';
|
||||||
|
|
||||||
export default (app) => {
|
export default (app) => {
|
||||||
app.use(router);
|
app.use(router);
|
||||||
app.use(store);
|
app.use(store);
|
||||||
|
app.component('DotDotBox', DotDotBox);
|
||||||
|
app.component('Popover', Popover);
|
||||||
|
app.component('ServerFlag', ServerFlag);
|
||||||
|
|
||||||
|
app.config.globalProperties.$hasSarasaTerm = !import.meta.env.VITE_DISABLE_SARASA_TERM_SC;
|
||||||
|
app.config.globalProperties.$config = config;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -24,7 +24,7 @@
|
|||||||
* - {string} core CPU核心信息
|
* - {string} core CPU核心信息
|
||||||
* - {string} cores CPU核心数
|
* - {string} cores CPU核心数
|
||||||
*/
|
*/
|
||||||
export function getCPUInfo(text) {
|
export function getCPUInfo(text = '') {
|
||||||
const cpuInfo = {
|
const cpuInfo = {
|
||||||
company: '',
|
company: '',
|
||||||
model: '',
|
model: '',
|
||||||
@ -45,41 +45,147 @@ export function getCPUInfo(text) {
|
|||||||
if (modelMatch) {
|
if (modelMatch) {
|
||||||
[cpuInfo.model] = modelMatch;
|
[cpuInfo.model] = modelMatch;
|
||||||
}
|
}
|
||||||
// 匹配特定的CPU型号编号
|
|
||||||
if (text.includes('Xeon')) {
|
|
||||||
if (text.includes('E-')) {
|
|
||||||
// Xeon型号
|
|
||||||
const modelNumReg = /(E\d-\S+)/;
|
|
||||||
const modelNumMatch = text.match(modelNumReg);
|
|
||||||
if (modelNumMatch) {
|
|
||||||
[, cpuInfo.modelNum] = modelNumMatch;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (text.includes('Gold')) {
|
|
||||||
// Xeon型号
|
|
||||||
const modelNumReg = /(Gold\s\w+)/;
|
|
||||||
const modelNumMatch = text.match(modelNumReg);
|
|
||||||
if (modelNumMatch) {
|
|
||||||
[, cpuInfo.modelNum] = modelNumMatch;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (text.includes('Ryzen')) {
|
if (text.includes('Ryzen')) {
|
||||||
// 5900X 5950X 7900X 7950X 9900X 9950X
|
// 匹配各种Ryzen型号:
|
||||||
const modelNumReg = /Ryzen.*(\d{4}X)/;
|
// - 标准型号: 5900X, 5950X, 7900X, 7950X, 9900X, 9950X
|
||||||
|
// - 普通型号: 3600, 5600, 7600
|
||||||
|
// - G系列APU: 5700G, 3400G
|
||||||
|
// - XT系列: 3600XT, 5600XT
|
||||||
|
// - 移动版: 4800U, 5800H, 6800HS
|
||||||
|
const modelNumReg = /Ryzen\s*(?:\d|(?:TR))\s*(?:\d{4}(?:[A-Z]{1,2})?)/;
|
||||||
const modelNumMatch = text.match(modelNumReg);
|
const modelNumMatch = text.match(modelNumReg);
|
||||||
if (modelNumMatch) {
|
if (modelNumMatch) {
|
||||||
[, cpuInfo.modelNum] = modelNumMatch;
|
cpuInfo.modelNum = modelNumMatch[0].replace(/Ryzen\s*(?:\d|(?:TR))\s*/, '');
|
||||||
|
} else {
|
||||||
|
// 备用正则表达式,尝试匹配其他可能的格式
|
||||||
|
const altModelNumReg = /Ryzen.*?(\d{3,4}(?:[A-Z]{0,2}))/;
|
||||||
|
const altModelNumMatch = text.match(altModelNumReg);
|
||||||
|
if (altModelNumMatch) {
|
||||||
|
[, cpuInfo.modelNum] = altModelNumMatch;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (text.includes('EPYC')) {
|
if (text.includes('EPYC')) {
|
||||||
// 7B13 7B13 9654...
|
// 匹配各种EPYC型号:
|
||||||
const modelNumReg = /EPYC (\w{4})/;
|
// - 第一代: 7001系列 (7351, 7551, 7601)
|
||||||
|
// - 第二代: 7002系列 (7252, 7542, 7742)
|
||||||
|
// - 第三代: 7003系列 (7313, 7543, 7763)
|
||||||
|
// - 第四代: 9004系列 (9124, 9354, 9654)
|
||||||
|
// - 特殊系列: 7Fxx, 7Hxx, 7Bxx (7F72, 7H12, 7B13)
|
||||||
|
const modelNumReg = /EPYC\s+(\d[A-Z0-9]{2,4})/i;
|
||||||
|
const modelNumMatch = text.match(modelNumReg);
|
||||||
|
if (modelNumMatch) {
|
||||||
|
[, cpuInfo.modelNum] = modelNumMatch;
|
||||||
|
} else {
|
||||||
|
// 备用匹配,处理可能的其他格式
|
||||||
|
const altModelNumReg = /EPYC.*?(\d{4,5}[A-Z]?)/i;
|
||||||
|
const altModelNumMatch = text.match(altModelNumReg);
|
||||||
|
if (altModelNumMatch) {
|
||||||
|
[, cpuInfo.modelNum] = altModelNumMatch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 匹配特定的CPU型号编号
|
||||||
|
if (text.includes('Xeon')) {
|
||||||
|
// 匹配所有Xeon处理器系列
|
||||||
|
// - E系列: E3, E5, E7等
|
||||||
|
// - 金属系列: Platinum, Gold, Silver, Bronze
|
||||||
|
// - 数字系列: W-1290, D-1653N等
|
||||||
|
// - 扩展名系列: L, X, M, D等(如X7560, L5640)
|
||||||
|
if (text.includes(' E')) {
|
||||||
|
const modelNumReg = /(E\d-\d{4}(?:\s?v\d)?)/;
|
||||||
|
const modelNumMatch = text.match(modelNumReg);
|
||||||
|
if (modelNumMatch) {
|
||||||
|
[, cpuInfo.modelNum] = modelNumMatch;
|
||||||
|
}
|
||||||
|
} else if (text.includes('Platinum')) {
|
||||||
|
const modelNumReg = /(?:Platinum\s+)(\d{4}(?:\w)?)/;
|
||||||
|
const modelNumMatch = text.match(modelNumReg);
|
||||||
|
if (modelNumMatch) {
|
||||||
|
[, cpuInfo.modelNum] = modelNumMatch;
|
||||||
|
}
|
||||||
|
} else if (text.includes('Gold')) {
|
||||||
|
const modelNumReg = /(?:Gold\s+)(\d{4}(?:\w)?)/;
|
||||||
|
const modelNumMatch = text.match(modelNumReg);
|
||||||
|
if (modelNumMatch) {
|
||||||
|
[, cpuInfo.modelNum] = modelNumMatch;
|
||||||
|
}
|
||||||
|
} else if (text.includes('Silver')) {
|
||||||
|
const modelNumReg = /(?:Silver\s+)(\d{4}(?:\w)?)/;
|
||||||
|
const modelNumMatch = text.match(modelNumReg);
|
||||||
|
if (modelNumMatch) {
|
||||||
|
[, cpuInfo.modelNum] = modelNumMatch;
|
||||||
|
}
|
||||||
|
} else if (text.includes('Bronze')) {
|
||||||
|
const modelNumReg = /(?:Bronze\s+)(\d{4}(?:\w)?)/;
|
||||||
|
const modelNumMatch = text.match(modelNumReg);
|
||||||
|
if (modelNumMatch) {
|
||||||
|
[, cpuInfo.modelNum] = modelNumMatch;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 通用Xeon型号匹配
|
||||||
|
const genericXeonReg = /Xeon(?:\(R\))?\s+(?:\w+-)?((?:W|D)?-?\d{4,5}(?:\w)?)/;
|
||||||
|
const genericMatch = text.match(genericXeonReg);
|
||||||
|
if (genericMatch) {
|
||||||
|
[, cpuInfo.modelNum] = genericMatch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (text.includes('Core')) {
|
||||||
|
if (text.includes('Core(TM)')) {
|
||||||
|
// 匹配如 Core(TM) i7-10700K 等格式
|
||||||
|
const modelNumReg = /Core\(TM\)\s+(\w\d+-\w+)/;
|
||||||
|
const modelNumMatch = text.match(modelNumReg);
|
||||||
|
if (modelNumMatch) {
|
||||||
|
[, cpuInfo.modelNum] = modelNumMatch;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 匹配如 Core i9-12900K, Core i5-13600K 等格式
|
||||||
|
const coreReg = /Core\s+(i[3579]-\d{4,5}(?:\w+)?)/i;
|
||||||
|
const coreMatch = text.match(coreReg);
|
||||||
|
if (coreMatch) {
|
||||||
|
[, cpuInfo.modelNum] = coreMatch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (text.includes('Celeron')) {
|
||||||
|
const modelNumReg = /Celeron(?:\(R\))?\s+(\w+\d+(?:\w+)?)/;
|
||||||
const modelNumMatch = text.match(modelNumReg);
|
const modelNumMatch = text.match(modelNumReg);
|
||||||
if (modelNumMatch) {
|
if (modelNumMatch) {
|
||||||
[, cpuInfo.modelNum] = modelNumMatch;
|
[, cpuInfo.modelNum] = modelNumMatch;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (text.includes('Pentium')) {
|
||||||
|
const modelNumReg = /Pentium(?:\(R\))?\s+(\w+\d+(?:\w+)?)/;
|
||||||
|
const modelNumMatch = text.match(modelNumReg);
|
||||||
|
if (modelNumMatch) {
|
||||||
|
[, cpuInfo.modelNum] = modelNumMatch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (text.includes('Intel(R) N')) {
|
||||||
|
const modelNumReg = /Intel\(R\)\s+(N\d+(?:\w+)?)/;
|
||||||
|
const modelNumMatch = text.match(modelNumReg);
|
||||||
|
if (modelNumMatch) {
|
||||||
|
[, cpuInfo.modelNum] = modelNumMatch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 匹配Apple M系列芯片
|
||||||
|
if (text.includes('Apple') && text.match(/M\d/)) {
|
||||||
|
// 匹配各种Apple Silicon M系列芯片:
|
||||||
|
// - 基本型号: M1, M2, M3等
|
||||||
|
// - 变种型号: M1 Pro, M2 Max, M3 Ultra等
|
||||||
|
const appleChipReg = /Apple\s+(?:Silicon\s+)?M(\d+(?:\s+(?:Pro|Max|Ultra|Extreme))?)/i;
|
||||||
|
const appleChipMatch = text.match(appleChipReg);
|
||||||
|
if (appleChipMatch) {
|
||||||
|
[, cpuInfo.modelNum] = appleChipMatch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (coresMatch) {
|
if (coresMatch) {
|
||||||
[cpuInfo.core, cpuInfo.cores] = coresMatch;
|
[cpuInfo.core, cpuInfo.cores] = coresMatch;
|
||||||
}
|
}
|
||||||
@ -164,11 +270,33 @@ export function calcTransfer(bytes) {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getPlatformLogoIconClassName(platform) {
|
||||||
|
const platformStr = (platform || '').toLowerCase();
|
||||||
|
if (platformStr.includes('windows') || platformStr.includes('microsoft')) {
|
||||||
|
return 'ri-microsoft-fill';
|
||||||
|
}
|
||||||
|
switch (platformStr) {
|
||||||
|
case 'darwin':
|
||||||
|
case 'macos':
|
||||||
|
return 'fl-apple';
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
if (platform) {
|
||||||
|
return `fl-${platform}`;
|
||||||
|
}
|
||||||
|
return 'ri-server-line';
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取系统发行版本
|
* 获取系统发行版本
|
||||||
*/
|
*/
|
||||||
export function getSystemOSLabel(platform) {
|
export function getSystemOSLabel(platform, short = false) {
|
||||||
switch (platform) {
|
const platformStr = (platform || '').toLowerCase();
|
||||||
|
// 匹配一些超长系统发行版本
|
||||||
|
if (short && platformStr.includes('windows')) {
|
||||||
|
return 'Windows';
|
||||||
|
}
|
||||||
|
switch (platformStr) {
|
||||||
case 'windows':
|
case 'windows':
|
||||||
return 'Windows';
|
return 'Windows';
|
||||||
case 'linux':
|
case 'linux':
|
||||||
|
|||||||
@ -1,40 +0,0 @@
|
|||||||
import config from '@/config';
|
|
||||||
|
|
||||||
const configReg = (type) => new RegExp(`${type} = JSON.parse\\('(.*)'\\)`);
|
|
||||||
// 格式化数据,保证JSON.parse能够正常解析
|
|
||||||
const unescaped = (str) => {
|
|
||||||
let str2 = str.replace(/\\u([\d\w]{4})/gi, (match, grp) => String.fromCharCode(parseInt(grp, 16)));
|
|
||||||
str2 = str2.replace(/\\\\r/g, '');
|
|
||||||
str2 = str2.replace(/\\\\n/g, '');
|
|
||||||
str2 = str2.replace(/\\\\/g, '\\');
|
|
||||||
return str2;
|
|
||||||
};
|
|
||||||
export default async () => fetch(config.nazhua.nezhaPath).then((res) => res.text()).then((res) => {
|
|
||||||
let resMatch = res?.match?.(configReg(config.nazhua.nezhaV0ConfigType));
|
|
||||||
// 尝试兼容不同的nezha前台主题
|
|
||||||
if (!resMatch) {
|
|
||||||
resMatch = res?.match?.(configReg(
|
|
||||||
config.nazhua.nezhaV1ConfigType === 'servers' ? 'initData' : 'servers',
|
|
||||||
));
|
|
||||||
}
|
|
||||||
const configStr = resMatch?.[1];
|
|
||||||
if (!configStr) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const remoteConfig = JSON.parse(unescaped(configStr));
|
|
||||||
if (remoteConfig?.servers) {
|
|
||||||
remoteConfig.servers = remoteConfig.servers.map((i) => {
|
|
||||||
const item = {
|
|
||||||
...i,
|
|
||||||
};
|
|
||||||
try {
|
|
||||||
item.PublicNote = JSON.parse(i.PublicNote);
|
|
||||||
} catch {
|
|
||||||
item.PublicNote = {};
|
|
||||||
}
|
|
||||||
return item;
|
|
||||||
});
|
|
||||||
return remoteConfig;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}).catch(() => null);
|
|
||||||
91
src/utils/load-nezha-v0-config.js
Normal file
91
src/utils/load-nezha-v0-config.js
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
import config from '@/config';
|
||||||
|
|
||||||
|
function getNezhaConfigUrl() {
|
||||||
|
const { nezhaPath } = config.nazhua;
|
||||||
|
if (nezhaPath.startsWith('http')) {
|
||||||
|
return nezhaPath;
|
||||||
|
}
|
||||||
|
const a = document.createElement('a');
|
||||||
|
if (nezhaPath === '/nezha/' && (import.meta.env.VITE_BASE_PATH && import.meta.env.VITE_BASE_PATH !== '/')) {
|
||||||
|
[a.href] = window.location.href.split(import.meta.env.VITE_BASE_PATH);
|
||||||
|
} else {
|
||||||
|
a.href = nezhaPath;
|
||||||
|
}
|
||||||
|
return a.href;
|
||||||
|
}
|
||||||
|
|
||||||
|
const configReg = (type) => new RegExp(`${type} = JSON.parse\\('(.*)'\\)`);
|
||||||
|
// 格式化数据,保证JSON.parse能够正常解析
|
||||||
|
const unescaped = (str) => {
|
||||||
|
let str2 = str.replace(/\\u([\d\w]{4})/gi, (match, grp) => String.fromCharCode(parseInt(grp, 16)));
|
||||||
|
str2 = str2.replace(/\\\\r/g, '');
|
||||||
|
str2 = str2.replace(/\\\\n/g, '');
|
||||||
|
str2 = str2.replace(/\\\\/g, '\\');
|
||||||
|
return str2;
|
||||||
|
};
|
||||||
|
export default async () => fetch(getNezhaConfigUrl()).then((res) => res.text()).then((res) => {
|
||||||
|
let resMatch = res?.match?.(configReg(config.nazhua.nezhaV0ConfigType));
|
||||||
|
// 尝试兼容不同的nezha前台主题
|
||||||
|
if (!resMatch) {
|
||||||
|
resMatch = res?.match?.(configReg(
|
||||||
|
config.nazhua.nezhaV1ConfigType === 'servers' ? 'initData' : 'servers',
|
||||||
|
));
|
||||||
|
}
|
||||||
|
const configStr = resMatch?.[1];
|
||||||
|
if (!configStr) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
let remoteConfig;
|
||||||
|
try {
|
||||||
|
remoteConfig = JSON.parse(unescaped(configStr));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to parse nezha config:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (remoteConfig?.servers) {
|
||||||
|
remoteConfig.servers = remoteConfig.servers.map((i) => {
|
||||||
|
const item = {
|
||||||
|
...i,
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
item.PublicNote = JSON.parse(i.PublicNote);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Failed to parse PublicNote for server:', i.ID || i.id, error);
|
||||||
|
item.PublicNote = {};
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
});
|
||||||
|
return remoteConfig;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}).catch((error) => {
|
||||||
|
console.error('Failed to load nezha config:', error);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取标签列表
|
||||||
|
*/
|
||||||
|
export const loadServerGroup = (services) => {
|
||||||
|
const tagMap = {};
|
||||||
|
services.forEach((i) => {
|
||||||
|
if (i.Tag) {
|
||||||
|
if (!tagMap[i.Tag]) {
|
||||||
|
tagMap[i.Tag] = [];
|
||||||
|
}
|
||||||
|
tagMap[i.Tag].push(i);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const tagList = [];
|
||||||
|
Object.entries(tagMap).forEach(([tag, serviceList]) => {
|
||||||
|
tagList.push({
|
||||||
|
name: tag,
|
||||||
|
count: serviceList.length,
|
||||||
|
servers: serviceList.map((i) => i.ID),
|
||||||
|
group: {
|
||||||
|
name: tag,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return tagList;
|
||||||
|
};
|
||||||
65
src/utils/load-nezha-v1-config.js
Normal file
65
src/utils/load-nezha-v1-config.js
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
/**
|
||||||
|
* V1版数据加载
|
||||||
|
*/
|
||||||
|
import config from '@/config';
|
||||||
|
import request from '@/utils/request';
|
||||||
|
|
||||||
|
export const loadServerGroup = async () => request({
|
||||||
|
// DELETE: v1GroupPath 兼容 v1ApiGroupPath 到v0.6.0
|
||||||
|
url: config.nazhua.v1GroupPath || config.nazhua.v1ApiGroupPath,
|
||||||
|
type: 'GET',
|
||||||
|
}).then((res) => {
|
||||||
|
if (res.status === 200 && res.data?.success) {
|
||||||
|
const list = res.data?.data || [];
|
||||||
|
return list.map((i) => {
|
||||||
|
const item = {
|
||||||
|
...i,
|
||||||
|
name: i?.group?.name,
|
||||||
|
count: i?.servers?.length,
|
||||||
|
};
|
||||||
|
return item;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}).catch((error) => {
|
||||||
|
console.error('Failed to load server group:', error);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载网站配置
|
||||||
|
*
|
||||||
|
* 暂时只使用site_name\custom_code
|
||||||
|
* 哪吒v1.4.9之后,上面的参数调整至data.config
|
||||||
|
*/
|
||||||
|
export const loadSetting = async () => request({
|
||||||
|
url: config.nazhua.v1ApiSettingPath,
|
||||||
|
type: 'GET',
|
||||||
|
}).then((res) => {
|
||||||
|
if (res.status === 200 && res.data?.success) {
|
||||||
|
return res.data?.data || {};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}).catch((error) => {
|
||||||
|
console.error('Failed to load setting:', error);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载个人信息
|
||||||
|
*/
|
||||||
|
export const loadProfile = async (check) => request({
|
||||||
|
url: config.nazhua.v1ApiProfilePath,
|
||||||
|
type: 'GET',
|
||||||
|
}).then((res) => {
|
||||||
|
if (check) {
|
||||||
|
return res.status === 200;
|
||||||
|
}
|
||||||
|
if (res.status === 200 && res.data?.success) {
|
||||||
|
return res.data?.data || {};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}).catch((error) => {
|
||||||
|
console.error('Failed to load profile:', error);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
99
src/utils/object-mapping.js
Normal file
99
src/utils/object-mapping.js
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
/**
|
||||||
|
* 对象映射封装
|
||||||
|
*/
|
||||||
|
class Mapping {
|
||||||
|
/**
|
||||||
|
* 字符串映射对象
|
||||||
|
*
|
||||||
|
* @param {Record<string, any>} obj 查找的对象
|
||||||
|
* @param {string} key 查找的属性
|
||||||
|
*
|
||||||
|
* @return {any}
|
||||||
|
*/
|
||||||
|
static mapping(obj, key) {
|
||||||
|
// 检查 obj 是否为对象,如果不是,返回 undefined
|
||||||
|
if (!obj || typeof obj !== 'object') {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
// 检查 key 是否为字符串,如果不是,返回 undefined
|
||||||
|
if (typeof key !== 'string') {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
// 检查 key 是否包含非法字符,如果包含,返回 undefined
|
||||||
|
if (key.includes('..') || key.startsWith('.') || key.endsWith('.')) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
// 如果 key 包含 '.',使用 reduce 方法递归获取嵌套属性值
|
||||||
|
if (key.includes('.')) {
|
||||||
|
return key.split('.').reduce((val, k) => (val !== undefined ? Mapping.get(val, k) : undefined), obj);
|
||||||
|
}
|
||||||
|
// 如果 key 不包含 '.',直接获取属性值
|
||||||
|
return Mapping.get(obj, key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取数据
|
||||||
|
* 支持处理数组指针
|
||||||
|
* @param {Record<string, any> | any[]} obj 属性对象
|
||||||
|
* @param {string} key 属性名称
|
||||||
|
* @return {any}
|
||||||
|
*/
|
||||||
|
static get(obj, key) {
|
||||||
|
if (!obj || typeof obj !== 'object' || !key) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const indexMatch = key.match(/\[(\d+)\]/);
|
||||||
|
if (indexMatch) {
|
||||||
|
const [fullMatch, indexStr] = indexMatch;
|
||||||
|
const index = Number(indexStr);
|
||||||
|
const matchIndex = key.indexOf(fullMatch);
|
||||||
|
if (matchIndex === 0) {
|
||||||
|
if (Array.isArray(obj) && index < obj.length) {
|
||||||
|
const val = obj[index];
|
||||||
|
const restKey = key.slice(fullMatch.length);
|
||||||
|
return restKey ? Mapping.get(val, restKey) : val;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const pre = key.slice(0, matchIndex);
|
||||||
|
const list = obj[pre];
|
||||||
|
if (Array.isArray(list) && index < list.length) {
|
||||||
|
const val = list[index];
|
||||||
|
const restKey = key.slice(matchIndex + fullMatch.length);
|
||||||
|
return restKey ? Mapping.get(val, restKey) : val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return obj[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 数据根据key的映射进行组装
|
||||||
|
*
|
||||||
|
* @param {KeyMap} keys 映射对象
|
||||||
|
* @param {Record<string, unknown>} data 数据对象
|
||||||
|
*
|
||||||
|
* @return {Record<string, unknown>}
|
||||||
|
*/
|
||||||
|
static each(keys, data) {
|
||||||
|
// 检查 keys 是否为对象,如果不是,返回 undefined
|
||||||
|
if (!keys || typeof keys !== 'object') {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
// 检查 data 是否为对象,如果不是,返回 undefined
|
||||||
|
if (!data || typeof data !== 'object') {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return Object.entries(keys).reduce((acc, [key, value]) => {
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
acc[key] = Mapping.mapping(data, value);
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const { mapping } = Mapping;
|
||||||
|
|
||||||
|
export { Mapping, mapping };
|
||||||
|
|
||||||
|
export default mapping;
|
||||||
6
src/utils/page-title.js
Normal file
6
src/utils/page-title.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import config from '@/config';
|
||||||
|
|
||||||
|
export default (...args) => {
|
||||||
|
const titles = [...new Set([...args, config.nazhua.title])].filter((i) => i);
|
||||||
|
document.title = titles.join(' - ');
|
||||||
|
};
|
||||||
@ -1,41 +1,27 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import uuid from '@/utils/uuid';
|
import uuid from '@/utils/uuid';
|
||||||
import validate from '@/utils/validate';
|
|
||||||
|
|
||||||
import config from '@/config';
|
|
||||||
import CustomError from './custom-error';
|
import CustomError from './custom-error';
|
||||||
|
|
||||||
const {
|
const limit = 10;
|
||||||
codeField,
|
|
||||||
dataField,
|
|
||||||
msgField,
|
|
||||||
okCode,
|
|
||||||
limit = 10,
|
|
||||||
} = config.request;
|
|
||||||
|
|
||||||
const requestTagMap = {};
|
const requestTagMap = {};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* axios请求
|
* axios请求
|
||||||
* @param {object} options 请求参数
|
* @param {object} options 请求参数
|
||||||
* @param {boolean} noFormat 不进行返回数据的格式化处理 网络状态200即为成功
|
|
||||||
* @return {Promise}
|
* @return {Promise}
|
||||||
*/
|
*/
|
||||||
async function axiosRequest(options, noFormat) {
|
async function axiosRequest(options) {
|
||||||
return axios(options).then((res) => {
|
return axios(options).then((res) => res).catch((err) => {
|
||||||
if (res.status === 200) {
|
if (err.response) {
|
||||||
if (noFormat) {
|
return err.response;
|
||||||
return res;
|
|
||||||
}
|
}
|
||||||
if (validate.isSet(res.data[codeField]) && `${res.data[codeField]}` === `${okCode}`) {
|
if (err.request) {
|
||||||
return res.data[dataField];
|
// 请求已经成功发起,但没有收到响应
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
if (typeof res.data[codeField] !== 'undefined') {
|
throw new CustomError(err.message);
|
||||||
throw new CustomError(res.data[msgField], res.data[codeField]);
|
|
||||||
}
|
|
||||||
throw new CustomError('服务器返回内容不规范', -99);
|
|
||||||
}
|
|
||||||
throw new CustomError(`网络错误${res.status}`, res.status);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -69,7 +55,6 @@ class NetworkRequest {
|
|||||||
* @param {string} type 请求的Method
|
* @param {string} type 请求的Method
|
||||||
* @param {object} headers Header请求参数
|
* @param {object} headers Header请求参数
|
||||||
* @param {object} data 请求参数
|
* @param {object} data 请求参数
|
||||||
* @param {boolean} noFormat 不进行返回数据的格式化处理 网络状态200即为成功
|
|
||||||
* @param {boolean} defaultContentType 默认的请求方式
|
* @param {boolean} defaultContentType 默认的请求方式
|
||||||
* @param {Boolean} priority 优先调用请求
|
* @param {Boolean} priority 优先调用请求
|
||||||
*
|
*
|
||||||
@ -85,7 +70,6 @@ class NetworkRequest {
|
|||||||
type,
|
type,
|
||||||
headers,
|
headers,
|
||||||
data,
|
data,
|
||||||
noFormat = false,
|
|
||||||
defaultContentType = true,
|
defaultContentType = true,
|
||||||
requestTag = undefined,
|
requestTag = undefined,
|
||||||
responseType,
|
responseType,
|
||||||
@ -100,9 +84,7 @@ class NetworkRequest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const defaultHeaders = {
|
const defaultHeaders = {};
|
||||||
...config.request.headers,
|
|
||||||
};
|
|
||||||
if (defaultContentType === false) {
|
if (defaultContentType === false) {
|
||||||
if (NetworkRequest.FormRequest(defaultHeaders)) {
|
if (NetworkRequest.FormRequest(defaultHeaders)) {
|
||||||
defaultHeaders['content-type'] = 'application/json';
|
defaultHeaders['content-type'] = 'application/json';
|
||||||
@ -122,7 +104,6 @@ class NetworkRequest {
|
|||||||
signal: abortController?.signal ?? undefined,
|
signal: abortController?.signal ?? undefined,
|
||||||
responseType,
|
responseType,
|
||||||
},
|
},
|
||||||
noFormat,
|
|
||||||
(res) => {
|
(res) => {
|
||||||
resolve(res);
|
resolve(res);
|
||||||
},
|
},
|
||||||
@ -153,13 +134,13 @@ class NetworkRequest {
|
|||||||
if (this.tasks.length === 0) {
|
if (this.tasks.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const [options, onFormat, success, fail, tag] = this.tasks.pop();
|
const [options, success, fail, tag] = this.tasks.pop();
|
||||||
// 请求未执行已被中止
|
// 请求未执行已被中止
|
||||||
if (options?.signal?.aborted) {
|
if (options?.signal?.aborted) {
|
||||||
this.overTask();
|
this.overTask();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
requestTagMap[tag] = axiosRequest(options, onFormat);
|
requestTagMap[tag] = axiosRequest(options);
|
||||||
requestTagMap[tag].finally(() => {
|
requestTagMap[tag].finally(() => {
|
||||||
this.overTask();
|
this.overTask();
|
||||||
// 一秒内请求不重复
|
// 一秒内请求不重复
|
||||||
|
|||||||
132
src/utils/transform-v1-2-v0.js
Normal file
132
src/utils/transform-v1-2-v0.js
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
/**
|
||||||
|
* V1版数据加载
|
||||||
|
*/
|
||||||
|
import store from '@/store';
|
||||||
|
import validate from '@/utils/validate';
|
||||||
|
import { Mapping } from '@/utils/object-mapping';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 字段映射
|
||||||
|
*/
|
||||||
|
export const SERVER_FIELD_MAPS = {
|
||||||
|
ID: 'id',
|
||||||
|
CreatedAt: undefined,
|
||||||
|
UpdatedAt: undefined,
|
||||||
|
DeletedAt: undefined,
|
||||||
|
Name: 'name',
|
||||||
|
Tag: '_$function|queryGroup|id',
|
||||||
|
DisplayIndex: 'display_index',
|
||||||
|
HideForGuest: undefined,
|
||||||
|
EnableDDNS: undefined,
|
||||||
|
Host: '_$mapping|HOST_FIELD_MAPS',
|
||||||
|
State: '_$mapping|STATE_FIELD_MAPS',
|
||||||
|
LastActive: 'last_active',
|
||||||
|
};
|
||||||
|
export const HOST_FIELD_MAPS = {
|
||||||
|
Platform: 'host.platform',
|
||||||
|
PlatformVersion: 'host.platform_version',
|
||||||
|
CPU: 'host.cpu',
|
||||||
|
MemTotal: 'host.mem_total',
|
||||||
|
DiskTotal: 'host.disk_total',
|
||||||
|
SwapTotal: 'host.swap_total',
|
||||||
|
Arch: 'host.arch',
|
||||||
|
Virtualization: 'host.virtualization',
|
||||||
|
BootTime: 'host.boot_time',
|
||||||
|
CountryCode: 'country_code',
|
||||||
|
Version: 'host.version',
|
||||||
|
GPU: 'host.gpu',
|
||||||
|
};
|
||||||
|
export const STATE_FIELD_MAPS = {
|
||||||
|
CPU: 'state.cpu',
|
||||||
|
MemUsed: 'state.mem_used',
|
||||||
|
SwapUsed: 'state.swap_used',
|
||||||
|
DiskUsed: 'state.disk_used',
|
||||||
|
NetInTransfer: 'state.net_in_transfer',
|
||||||
|
NetOutTransfer: 'state.net_out_transfer',
|
||||||
|
NetInSpeed: 'state.net_in_speed',
|
||||||
|
NetOutSpeed: 'state.net_out_speed',
|
||||||
|
Uptime: 'state.uptime',
|
||||||
|
Load1: 'state.load_1',
|
||||||
|
Load5: 'state.load_5',
|
||||||
|
Load15: 'state.load_15',
|
||||||
|
TcpConnCount: 'state.tcp_conn_count',
|
||||||
|
UdpConnCount: 'state.udp_conn_count',
|
||||||
|
ProcessCount: 'state.process_count',
|
||||||
|
Temperatures: 'state.temperatures',
|
||||||
|
GPU: 'state.gpu',
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 魔法方法
|
||||||
|
*/
|
||||||
|
const magics = {
|
||||||
|
HOST_FIELD_MAPS,
|
||||||
|
STATE_FIELD_MAPS,
|
||||||
|
queryGroup: (id) => {
|
||||||
|
const groupItem = store.state.serverGroup?.find?.((i) => {
|
||||||
|
if (i.servers) {
|
||||||
|
return i.servers.includes(id);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
return groupItem?.name;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理V1版数据
|
||||||
|
* @param {Object} v1Data V1版数据
|
||||||
|
* @return {Object} V0版数据
|
||||||
|
*/
|
||||||
|
export default function (v1Data) {
|
||||||
|
const v0Data = {};
|
||||||
|
Object.keys(SERVER_FIELD_MAPS).forEach((key) => {
|
||||||
|
if (SERVER_FIELD_MAPS[key] === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (SERVER_FIELD_MAPS[key].includes('_$')) {
|
||||||
|
const $magic = SERVER_FIELD_MAPS[key].split('|');
|
||||||
|
switch ($magic[0]) {
|
||||||
|
case '_$function':
|
||||||
|
if ($magic.length >= 3 && magics[$magic[1]]) {
|
||||||
|
v0Data[key] = magics[$magic[1]](
|
||||||
|
Mapping.mapping(v1Data, $magic[2]),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
v0Data[key] = undefined;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case '_$mapping':
|
||||||
|
v0Data[key] = Mapping.each(magics[$magic[1]], v1Data);
|
||||||
|
if (key === 'State') {
|
||||||
|
// 修复Load1、Load5、Load15字段为空时的问题
|
||||||
|
[
|
||||||
|
'Load1', 'Load5', 'Load15',
|
||||||
|
'NetInTransfer', 'NetOutTransfer',
|
||||||
|
'NetInSpeed', 'NetOutSpeed',
|
||||||
|
].forEach((k) => {
|
||||||
|
if (!validate.isSet(v0Data[key][k])) {
|
||||||
|
v0Data[key][k] = 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
v0Data[key] = Mapping.mapping(v1Data, SERVER_FIELD_MAPS[key]);
|
||||||
|
});
|
||||||
|
if (v1Data.public_note) {
|
||||||
|
try {
|
||||||
|
v0Data.PublicNote = JSON.parse(v1Data.public_note);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Failed to parse public_note for server:', v1Data.id, e);
|
||||||
|
v0Data.PublicNote = null;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
v0Data.PublicNote = null;
|
||||||
|
}
|
||||||
|
return v0Data;
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
13
src/utils/zIndexManager.js
Normal file
13
src/utils/zIndexManager.js
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
const BASE_Z_INDEX = 1000;
|
||||||
|
let zIndexCounter = BASE_Z_INDEX;
|
||||||
|
|
||||||
|
export const getNextZIndex = () => {
|
||||||
|
zIndexCounter += 1;
|
||||||
|
return zIndexCounter;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getCurrentZIndex = () => zIndexCounter;
|
||||||
|
|
||||||
|
export const resetZIndex = () => {
|
||||||
|
zIndexCounter = BASE_Z_INDEX;
|
||||||
|
};
|
||||||
@ -1,32 +1,113 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="server-info-box">
|
<dot-dot-box class="server-info-box">
|
||||||
<div class="server-info-group server-info--cpu">
|
<div class="server-info-group server-info--cpu">
|
||||||
<div class="server-info-label">
|
<div class="server-info-label">
|
||||||
CPU
|
CPU
|
||||||
</div>
|
</div>
|
||||||
<div class="server-info-content">
|
<div class="server-info-content">
|
||||||
|
<template v-if="info?.Host?.CPU?.length === 1">
|
||||||
<span
|
<span
|
||||||
class="cpu-info"
|
class="cpu-info"
|
||||||
:title="info?.Host?.CPU?.[0]"
|
:title="info.Host.CPU[0]"
|
||||||
>
|
>
|
||||||
<span>{{ info?.Host?.CPU?.[0] }}</span>
|
<span>{{ info.Host.CPU[0] }}</span>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="server-info-item-group"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
v-for="(cpuItem, cpuIndex) in info.Host.CPU"
|
||||||
|
:key="`${info.ID}_cpu_${cpuIndex}`"
|
||||||
|
class="server-info-item"
|
||||||
|
>
|
||||||
|
<span class="server-info-item-label">CPU.{{ cpuIndex + 1 }}</span>
|
||||||
|
<span class="server-info-item-value">{{ cpuItem }}</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="info?.Host?.GPU?.[0]"
|
v-if="gpuList.length"
|
||||||
class="server-info-group server-info--gpu"
|
class="server-info-group server-info--gpu"
|
||||||
>
|
>
|
||||||
<div class="server-info-label">
|
<div class="server-info-label">
|
||||||
GPU
|
GPU
|
||||||
</div>
|
</div>
|
||||||
<div class="server-info-content">
|
<div class="server-info-content">
|
||||||
|
<template v-if="gpuList.length === 1">
|
||||||
<span
|
<span
|
||||||
class="cpu-info"
|
class="gpu-info"
|
||||||
:title="info?.Host?.GPU?.[0]"
|
:title="gpuList[0]"
|
||||||
>
|
>
|
||||||
<span>{{ info?.Host?.GPU?.[0] }}</span>
|
<span>{{ gpuList[0] }}</span>
|
||||||
</span>
|
</span>
|
||||||
|
</template>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="server-info-item-group"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
v-for="(gpuItem, gpuIndex) in gpuList"
|
||||||
|
:key="`${info.ID}_gpu_${gpuIndex}`"
|
||||||
|
class="server-info-item"
|
||||||
|
>
|
||||||
|
<span class="server-info-item-label">GPU.{{ gpuIndex + 1 }}</span>
|
||||||
|
<span class="server-info-item-value">{{ gpuItem }}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="temperatureData.list.length"
|
||||||
|
class="server-info-group server-info--temperature"
|
||||||
|
>
|
||||||
|
<div class="server-info-label">
|
||||||
|
温度
|
||||||
|
</div>
|
||||||
|
<div class="server-info-content">
|
||||||
|
<div class="server-info-item-group">
|
||||||
|
<template
|
||||||
|
v-for="(ttItem, ttIndex) in temperatureData.list"
|
||||||
|
:key="`${info.ID}_temperature_${ttIndex}`"
|
||||||
|
>
|
||||||
|
<popover :title="ttItem?.title || (`${ttItem.label}: ${ttItem.value}`)">
|
||||||
|
<template #trigger>
|
||||||
|
<span
|
||||||
|
class="server-info-item"
|
||||||
|
:class="`temperature--${ttItem.type}`"
|
||||||
|
>
|
||||||
|
<span class="server-info-item-icon">
|
||||||
|
<i
|
||||||
|
v-if="ttItem.type === 'cpu' || ttItem.label.toLowerCase().includes('cpu')"
|
||||||
|
class="ri-cpu-line"
|
||||||
|
/>
|
||||||
|
<i
|
||||||
|
v-else-if="ttItem.type === 'gpu' || ttItem.label.toLowerCase().includes('gpu')"
|
||||||
|
class="ri-gamepad-line"
|
||||||
|
/>
|
||||||
|
<i
|
||||||
|
v-else-if="ttItem.type === 'nvme' || ttItem.label.toLowerCase().includes('nvme')"
|
||||||
|
class="ri-hard-drive-3-line"
|
||||||
|
/>
|
||||||
|
<i
|
||||||
|
v-else-if="ttItem.type === 'motherboard'"
|
||||||
|
class="ri-instance-line"
|
||||||
|
/>
|
||||||
|
<i
|
||||||
|
v-else
|
||||||
|
class="ri-temp-hot-line"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<span class="server-info-item-value">
|
||||||
|
{{ ttItem.value }}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</popover>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="server-info-group server-info--system-os">
|
<div class="server-info-group server-info--system-os">
|
||||||
@ -50,7 +131,7 @@
|
|||||||
占用
|
占用
|
||||||
</div>
|
</div>
|
||||||
<div class="server-info-content">
|
<div class="server-info-content">
|
||||||
<span class="server-info-item-group">
|
<div class="server-info-item-group">
|
||||||
<span class="server-info-item process-count">
|
<span class="server-info-item process-count">
|
||||||
<span class="server-info-item-label">进程数</span>
|
<span class="server-info-item-label">进程数</span>
|
||||||
<span class="server-info-item-value">{{ processCount }}</span>
|
<span class="server-info-item-value">{{ processCount }}</span>
|
||||||
@ -58,18 +139,18 @@
|
|||||||
<span class="server-info-item load">
|
<span class="server-info-item load">
|
||||||
<span class="server-info-item-label">负载</span>
|
<span class="server-info-item-label">负载</span>
|
||||||
<span class="server-info-item-value">
|
<span class="server-info-item-value">
|
||||||
{{ info?.State?.Load1 }},{{ info?.State?.Load5 }},{{ info?.State?.Load15 }}
|
{{ sysLoadInfo }}
|
||||||
</span>
|
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div class="server-info-group server-info--transfer">
|
<div class="server-info-group server-info--transfer">
|
||||||
<div class="server-info-label">
|
<div class="server-info-label">
|
||||||
流量
|
流量
|
||||||
</div>
|
</div>
|
||||||
<div class="server-info-content">
|
<div class="server-info-content">
|
||||||
<span class="server-info-item-group">
|
<div class="server-info-item-group">
|
||||||
<span class="server-info-item transfer--in">
|
<span class="server-info-item transfer--in">
|
||||||
<span class="server-info-item-label">入网</span>
|
<span class="server-info-item-label">入网</span>
|
||||||
<span class="server-info-item-value">
|
<span class="server-info-item-value">
|
||||||
@ -84,7 +165,7 @@
|
|||||||
<span class="text-unit">{{ transfer?.out?.unit }}</span>
|
<span class="text-unit">{{ transfer?.out?.unit }}</span>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="server-info-group server-info--conn">
|
<div class="server-info-group server-info--conn">
|
||||||
@ -92,7 +173,7 @@
|
|||||||
连接
|
连接
|
||||||
</div>
|
</div>
|
||||||
<div class="server-info-content">
|
<div class="server-info-content">
|
||||||
<span class="server-info-item-group">
|
<div class="server-info-item-group">
|
||||||
<span class="server-info-item conn--tcp">
|
<span class="server-info-item conn--tcp">
|
||||||
<span class="server-info-item-label">TCP</span>
|
<span class="server-info-item-label">TCP</span>
|
||||||
<span class="server-info-item-value">{{ tcpConnCount }}</span>
|
<span class="server-info-item-value">{{ tcpConnCount }}</span>
|
||||||
@ -101,7 +182,7 @@
|
|||||||
<span class="server-info-item-label">UDP</span>
|
<span class="server-info-item-label">UDP</span>
|
||||||
<span class="server-info-item-value">{{ udpConnCount }}</span>
|
<span class="server-info-item-value">{{ udpConnCount }}</span>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="server-info-group server-info--boottime">
|
<div class="server-info-group server-info--boottime">
|
||||||
@ -132,7 +213,7 @@
|
|||||||
套餐
|
套餐
|
||||||
</div>
|
</div>
|
||||||
<div class="server-info-content">
|
<div class="server-info-content">
|
||||||
<span class="server-info-item-group">
|
<div class="server-info-item-group">
|
||||||
<span
|
<span
|
||||||
v-for="item in billPlanData"
|
v-for="item in billPlanData"
|
||||||
:key="item.label"
|
:key="item.label"
|
||||||
@ -144,7 +225,7 @@
|
|||||||
>{{ item.label }}</span>
|
>{{ item.label }}</span>
|
||||||
<span class="server-info-item-value">{{ item.value }}</span>
|
<span class="server-info-item-value">{{ item.value }}</span>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@ -160,6 +241,9 @@
|
|||||||
v-for="(tag, index) in tagList"
|
v-for="(tag, index) in tagList"
|
||||||
:key="`${tag}_${index}`"
|
:key="`${tag}_${index}`"
|
||||||
class="server-info-tag-item"
|
class="server-info-tag-item"
|
||||||
|
:class="{
|
||||||
|
'has-sarasa-term': $hasSarasaTerm && config.nazhua.disableSarasaTermSC !== true,
|
||||||
|
}"
|
||||||
>
|
>
|
||||||
{{ tag }}
|
{{ tag }}
|
||||||
</span>
|
</span>
|
||||||
@ -176,13 +260,13 @@
|
|||||||
@click.stop="toBuy"
|
@click.stop="toBuy"
|
||||||
>
|
>
|
||||||
<span class="icon">
|
<span class="icon">
|
||||||
<span class="ri-shopping-bag-3-line" />
|
<span :class="buyBtnIcon" />
|
||||||
</span>
|
</span>
|
||||||
<span class="text">{{ buyBtnText }}</span>
|
<span class="text">{{ buyBtnText }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</dot-dot-box>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
@ -205,7 +289,18 @@ const props = defineProps({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const buyBtnText = config.nazhua.buyBtnText || '购买';
|
const buyBtnIcon = computed(() => {
|
||||||
|
if (props.info?.PublicNote?.customData?.buyBtnIcon) {
|
||||||
|
return props.info?.PublicNote?.customData?.buyBtnIcon;
|
||||||
|
}
|
||||||
|
return config.nazhua.buyBtnIcon || 'ri-shopping-bag-3-line';
|
||||||
|
});
|
||||||
|
const buyBtnText = computed(() => {
|
||||||
|
if (props.info?.PublicNote?.customData?.buyBtnText) {
|
||||||
|
return props.info?.PublicNote?.customData?.buyBtnText;
|
||||||
|
}
|
||||||
|
return config.nazhua.buyBtnText || '购买';
|
||||||
|
});
|
||||||
const showBuyBtn = computed(() => !!props.info?.PublicNote?.customData?.orderLink);
|
const showBuyBtn = computed(() => !!props.info?.PublicNote?.customData?.orderLink);
|
||||||
|
|
||||||
function toBuy() {
|
function toBuy() {
|
||||||
@ -213,6 +308,200 @@ function toBuy() {
|
|||||||
window.open(decodeUrl, '_blank');
|
window.open(decodeUrl, '_blank');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GPU列表
|
||||||
|
*/
|
||||||
|
const gpuList = computed(() => {
|
||||||
|
const gpus = props.info?.Host?.GPU || [];
|
||||||
|
if (config.nazhua?.filterGPUKeywords?.length) {
|
||||||
|
// 过滤奇怪的GPU,可以考虑过滤掉 Virtual Display
|
||||||
|
const keywors = Array.isArray(config.nazhua.filterGPUKeywords)
|
||||||
|
? config.nazhua.filterGPUKeywords
|
||||||
|
: [config.nazhua.filterGPUKeywords];
|
||||||
|
return gpus.filter((i) => {
|
||||||
|
if (keywors.length) {
|
||||||
|
return !keywors.some((k) => i.toLowerCase().includes(k.toLowerCase()));
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return gpus;
|
||||||
|
});
|
||||||
|
|
||||||
|
const sysLoadInfo = computed(() => {
|
||||||
|
if (props.info?.State?.Load1 !== undefined) {
|
||||||
|
return [
|
||||||
|
props.info.State?.Load1,
|
||||||
|
props.info.State?.Load5,
|
||||||
|
props.info.State?.Load15,
|
||||||
|
].filter((i) => i !== undefined).map((i) => (i).toFixed(2) * 1).join(',');
|
||||||
|
}
|
||||||
|
return '-';
|
||||||
|
});
|
||||||
|
|
||||||
|
const temperatureData = computed(() => {
|
||||||
|
const data = [];
|
||||||
|
if (props.info?.State?.Temperatures) {
|
||||||
|
const acpitz = [];
|
||||||
|
const coretemp_package_id = [];
|
||||||
|
const coretemp_core = [];
|
||||||
|
const nvme = [];
|
||||||
|
const k10temp = [];
|
||||||
|
const amdgpu = [];
|
||||||
|
const other = [];
|
||||||
|
|
||||||
|
// 温度数据分类处理
|
||||||
|
props.info.State.Temperatures.forEach((item) => {
|
||||||
|
const name = item.Name.toLowerCase();
|
||||||
|
const temp = item.Temperature;
|
||||||
|
|
||||||
|
if (name.startsWith('acpitz')) {
|
||||||
|
acpitz.push(temp);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (name.startsWith('coretemp_package_id_')) {
|
||||||
|
const coreIndex = parseInt(name.replace('coretemp_package_id_', ''), 10);
|
||||||
|
coretemp_package_id.push({
|
||||||
|
index: coreIndex,
|
||||||
|
value: temp,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (name.startsWith('coretemp_core_')) {
|
||||||
|
const coreIndex = parseInt(name.replace('coretemp_core_', ''), 10);
|
||||||
|
coretemp_core.push({
|
||||||
|
index: coreIndex,
|
||||||
|
value: temp,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (name.includes('nvme')) {
|
||||||
|
nvme.push({
|
||||||
|
name: item.Name,
|
||||||
|
value: temp,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (name.includes('k10temp')) {
|
||||||
|
k10temp.push({
|
||||||
|
name: item.Name,
|
||||||
|
value: temp,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (name.includes('amdgpu')) {
|
||||||
|
amdgpu.push({
|
||||||
|
name: item.Name,
|
||||||
|
value: temp,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (name.includes('motherboard') || name.includes('mainboard') || name.includes('board')) {
|
||||||
|
other.push({
|
||||||
|
label: '主板',
|
||||||
|
value: temp,
|
||||||
|
type: 'motherboard',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
other.push({
|
||||||
|
label: item.Name,
|
||||||
|
value: temp,
|
||||||
|
type: 'other',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 主板温度处理
|
||||||
|
if (acpitz.length) {
|
||||||
|
const acpitzMean = (acpitz.reduce((a, b) => a + b, 0) / acpitz.length).toFixed(1);
|
||||||
|
data.push({
|
||||||
|
label: '主板',
|
||||||
|
value: `${acpitzMean}℃`,
|
||||||
|
title: acpitz.map((i, index) => `传感器${index + 1}: ${parseFloat(i).toFixed(1)}℃`).join('\n'),
|
||||||
|
type: 'motherboard',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// CPU温度处理
|
||||||
|
if (coretemp_package_id.length || coretemp_core.length) {
|
||||||
|
const temps = [];
|
||||||
|
const details = [];
|
||||||
|
|
||||||
|
// 处理 CPU 温度
|
||||||
|
if (coretemp_package_id.length) {
|
||||||
|
const cpuTemps = coretemp_package_id.map((i) => `${parseFloat(i.value).toFixed(1)}℃`);
|
||||||
|
temps.push(cpuTemps.join(', '));
|
||||||
|
details.push(...coretemp_package_id.map((i) => `CPU.${i.index + 1}: ${parseFloat(i.value).toFixed(1)}℃`));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理核心温度
|
||||||
|
if (coretemp_core.length) {
|
||||||
|
const coreMean = (coretemp_core.reduce((a, b) => a + b.value, 0) / coretemp_core.length).toFixed(1);
|
||||||
|
temps.push(`${parseFloat(coreMean).toFixed(1)}℃`);
|
||||||
|
details.push(...coretemp_core.map((i) => `核心${i.index + 1}: ${parseFloat(i.value).toFixed(1)}℃`));
|
||||||
|
}
|
||||||
|
|
||||||
|
data.push({
|
||||||
|
label: 'CPU',
|
||||||
|
value: temps.join(' / '),
|
||||||
|
title: details.join('\n'),
|
||||||
|
type: 'cpu',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// AMD CPU温度处理
|
||||||
|
if (k10temp.length) {
|
||||||
|
const tctl = k10temp.find((i) => i.name.includes('tctl'));
|
||||||
|
if (tctl) {
|
||||||
|
data.push({
|
||||||
|
label: 'AMD CPU',
|
||||||
|
value: `${parseFloat(tctl.value).toFixed(1)}℃`,
|
||||||
|
title: k10temp.map((i) => `${i.name}: ${parseFloat(i.value).toFixed(1)}℃`).join('\n'),
|
||||||
|
type: 'cpu',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AMD GPU温度处理
|
||||||
|
if (amdgpu.length) {
|
||||||
|
const edge = amdgpu.find((i) => i.name.includes('edge'));
|
||||||
|
if (edge) {
|
||||||
|
data.push({
|
||||||
|
label: 'AMD GPU',
|
||||||
|
value: `${parseFloat(edge.value).toFixed(1)}℃`,
|
||||||
|
title: amdgpu.map((i) => `${i.name}: ${parseFloat(i.value).toFixed(1)}℃`).join('\n'),
|
||||||
|
type: 'gpu',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NVME温度处理
|
||||||
|
if (nvme.length) {
|
||||||
|
const composite = nvme.find((i) => i.name.includes('composite'));
|
||||||
|
if (composite) {
|
||||||
|
data.push({
|
||||||
|
label: 'NVME',
|
||||||
|
value: `${parseFloat(composite.value).toFixed(1)}℃`,
|
||||||
|
title: nvme.map((i) => `${i.name}: ${parseFloat(i.value).toFixed(1)}℃`).join('\n'),
|
||||||
|
type: 'nvme',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 其他温度处理
|
||||||
|
other.forEach((i) => {
|
||||||
|
data.push({
|
||||||
|
label: i.label,
|
||||||
|
value: `${parseFloat(i.value).toFixed(1)}℃`,
|
||||||
|
type: i.type || 'other',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
list: data,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
const {
|
const {
|
||||||
billAndPlan,
|
billAndPlan,
|
||||||
} = handleServerBillAndPlan({
|
} = handleServerBillAndPlan({
|
||||||
@ -231,11 +520,24 @@ const billPlanData = computed(() => ['billing', 'remainingTime', 'bandwidth', 't
|
|||||||
|
|
||||||
const tagList = computed(() => {
|
const tagList = computed(() => {
|
||||||
const list = [];
|
const list = [];
|
||||||
if (props?.info?.PublicNote?.planDataMod?.networkRoute) {
|
const {
|
||||||
list.push(...props.info.PublicNote.planDataMod.networkRoute.split(','));
|
networkRoute,
|
||||||
|
extra,
|
||||||
|
IPv4,
|
||||||
|
IPv6,
|
||||||
|
} = props?.info?.PublicNote?.planDataMod || {};
|
||||||
|
if (networkRoute) {
|
||||||
|
list.push(...networkRoute?.split?.(','));
|
||||||
}
|
}
|
||||||
if (props?.info?.PublicNote?.planDataMod?.extra) {
|
if (extra) {
|
||||||
list.push(...props.info.PublicNote.planDataMod.extra.split(','));
|
list.push(...extra?.split?.(','));
|
||||||
|
}
|
||||||
|
if (IPv4 === '1' && IPv6 === '1') {
|
||||||
|
list.push('双栈IP');
|
||||||
|
} else if (IPv4 === '1') {
|
||||||
|
list.push('仅IPv4');
|
||||||
|
} else if (IPv6 === '1') {
|
||||||
|
list.push('仅IPv6');
|
||||||
}
|
}
|
||||||
return list;
|
return list;
|
||||||
});
|
});
|
||||||
@ -296,19 +598,6 @@ const processCount = computed(() => props.info?.State?.ProcessCount);
|
|||||||
.server-info-box {
|
.server-info-box {
|
||||||
--server-info-item-size: 24px;
|
--server-info-item-size: 24px;
|
||||||
|
|
||||||
padding: 20px;
|
|
||||||
color: #eee;
|
|
||||||
border-radius: 12px;
|
|
||||||
background-image: radial-gradient(transparent 1px, rgba(#000, 0.6) 1px);
|
|
||||||
background-size: 3px 3px;
|
|
||||||
backdrop-filter: saturate(50%) blur(3px);
|
|
||||||
box-shadow: 2px 4px 6px rgba(#000, 0.4);
|
|
||||||
|
|
||||||
@media screen and (max-width: 768px) {
|
|
||||||
background-color: rgba(#000, 0.8);
|
|
||||||
background-image: none;
|
|
||||||
backdrop-filter: none;
|
|
||||||
}
|
|
||||||
@media screen and (max-width: 480px) {
|
@media screen and (max-width: 480px) {
|
||||||
--server-info-item-size: 30px;
|
--server-info-item-size: 30px;
|
||||||
}
|
}
|
||||||
@ -344,11 +633,29 @@ const processCount = computed(() => props.info?.State?.ProcessCount);
|
|||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 0 12px;
|
gap: 0 12px;
|
||||||
|
|
||||||
|
&.temperature--other {
|
||||||
|
// 移动端不显示
|
||||||
|
@media screen and (max-width: 768px) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.server-info-item {
|
.server-info-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.2em;
|
gap: 0.2em;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.server-info-item-icon {
|
||||||
|
width: 24px;
|
||||||
|
height: 16px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 16px;
|
||||||
|
color: #ccc;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.server-info-item-value {
|
.server-info-item-value {
|
||||||
@ -373,6 +680,17 @@ const processCount = computed(() => props.info?.State?.ProcessCount);
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.server-info--temperature {
|
||||||
|
.server-info-item {
|
||||||
|
.server-info-item-label {
|
||||||
|
max-width: 4.5em;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.server-info--order-link {
|
.server-info--order-link {
|
||||||
padding: 10px 0 0;
|
padding: 10px 0 0;
|
||||||
}
|
}
|
||||||
@ -414,11 +732,16 @@ const processCount = computed(() => props.info?.State?.ProcessCount);
|
|||||||
.server-info-tag-item {
|
.server-info-tag-item {
|
||||||
height: 18px;
|
height: 18px;
|
||||||
padding: 0 5px 0 6px;
|
padding: 0 5px 0 6px;
|
||||||
line-height: 20px;
|
line-height: 18px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: var(--public-note-tag-color);
|
color: var(--public-note-tag-color);
|
||||||
background-color: var(--public-note-tag-bg);
|
background: var(--public-note-tag-bg);
|
||||||
|
text-shadow: 1px 1px 2px rgba(#000, 0.2);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
|
||||||
|
&.has-sarasa-term {
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<dot-dot-box
|
||||||
v-if="monitorData.length"
|
v-if="monitorData.length"
|
||||||
class="server-monitor-group"
|
class="server-monitor-group"
|
||||||
|
:class="{
|
||||||
|
'chart-type--multi': config.nazhua.monitorChartTypeToggle && monitorChartType === 'multi',
|
||||||
|
'chart-type--single': config.nazhua.monitorChartTypeToggle && monitorChartType === 'single',
|
||||||
|
}"
|
||||||
|
padding="16px 20px"
|
||||||
>
|
>
|
||||||
<div class="module-head-group">
|
<div class="module-head-group">
|
||||||
<div class="left-box">
|
<div class="left-box">
|
||||||
@ -10,11 +15,43 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="right-box">
|
<div class="right-box">
|
||||||
|
<div
|
||||||
|
v-if="config.nazhua.monitorChartTypeToggle"
|
||||||
|
class="chart-type-switch-group"
|
||||||
|
title="监控折线图是否聚合"
|
||||||
|
@click="switchChartType"
|
||||||
|
>
|
||||||
|
<span class="label-text">聚合</span>
|
||||||
|
<div
|
||||||
|
class="switch-box"
|
||||||
|
:class="{
|
||||||
|
active: monitorChartType === 'multi',
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<span class="switch-dot" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="refresh-data-group"
|
||||||
|
title="是否自动刷新"
|
||||||
|
@click="switchRefresh"
|
||||||
|
>
|
||||||
|
<span class="label-text">刷新</span>
|
||||||
|
<div
|
||||||
|
class="switch-box"
|
||||||
|
:class="{
|
||||||
|
active: refreshData,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<span class="switch-dot" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
class="peak-shaving-group"
|
class="peak-shaving-group"
|
||||||
title="过滤太高或太低的数据"
|
title="过滤太高或太低的数据"
|
||||||
@click="switchPeakShaving"
|
@click="switchPeakShaving"
|
||||||
>
|
>
|
||||||
|
<span class="label-text">削峰</span>
|
||||||
<div
|
<div
|
||||||
class="switch-box"
|
class="switch-box"
|
||||||
:class="{
|
:class="{
|
||||||
@ -23,16 +60,128 @@
|
|||||||
>
|
>
|
||||||
<span class="switch-dot" />
|
<span class="switch-dot" />
|
||||||
</div>
|
</div>
|
||||||
<span class="label-text">削峰</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="last-update-time-group">
|
||||||
|
<span class="last-update-time-label">
|
||||||
|
最近
|
||||||
|
</span>
|
||||||
|
<div class="minutes">
|
||||||
|
<div
|
||||||
|
v-for="minuteItem in minutes"
|
||||||
|
:key="minuteItem.value"
|
||||||
|
class="minute-item"
|
||||||
|
:class="{
|
||||||
|
active: minuteItem.value === minute,
|
||||||
|
}"
|
||||||
|
@click="toggleMinute(minuteItem.value)"
|
||||||
|
>
|
||||||
|
<span>{{ minuteItem.label }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div
|
||||||
<line-chart
|
class="active-arrow"
|
||||||
:cate-list="monitorChartData.cateList"
|
:style="minuteActiveArrowStyle"
|
||||||
:date-list="monitorChartData.dateList"
|
|
||||||
:value-list="monitorChartData.valueList"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template v-if="monitorChartType === 'single'">
|
||||||
|
<div class="monitor-chart-group">
|
||||||
|
<div
|
||||||
|
v-for="(cateItem, index) in monitorChartData.cateList"
|
||||||
|
:key="cateItem.id"
|
||||||
|
class="monitor-chart-item"
|
||||||
|
>
|
||||||
|
<div class="cate-name-box">
|
||||||
|
<popover :title="cateItem.title">
|
||||||
|
<template #trigger>
|
||||||
|
<div
|
||||||
|
class="monitor-cate-item"
|
||||||
|
:class="{
|
||||||
|
disabled: showCates[cateItem.id] === false,
|
||||||
|
}"
|
||||||
|
:style="{
|
||||||
|
'--cate-color': cateItem.color,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<span class="cate-legend" />
|
||||||
|
<span
|
||||||
|
class="cate-name"
|
||||||
|
>
|
||||||
|
{{ cateItem.name }}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-if="cateItem.avg !== 0"
|
||||||
|
class="cate-avg-ms"
|
||||||
|
>
|
||||||
|
{{ cateItem.avg }}ms
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-if="cateItem.over !== 0"
|
||||||
|
class="cate-over-rate"
|
||||||
|
>
|
||||||
|
{{ cateItem.over }}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</popover>
|
||||||
|
</div>
|
||||||
|
<line-chart
|
||||||
|
:date-list="monitorChartData.dateList"
|
||||||
|
:value-list="[monitorChartData.valueList[index]]"
|
||||||
|
:size="240"
|
||||||
|
:connect-nulls="false"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<div class="monitor-cate-group">
|
||||||
|
<template
|
||||||
|
v-for="cateItem in monitorChartData.cateList"
|
||||||
|
:key="cateItem.id"
|
||||||
|
>
|
||||||
|
<popover :title="cateItem.title">
|
||||||
|
<template #trigger>
|
||||||
|
<div
|
||||||
|
class="monitor-cate-item"
|
||||||
|
:class="{
|
||||||
|
disabled: showCates[cateItem.id] === false,
|
||||||
|
}"
|
||||||
|
:style="{
|
||||||
|
'--cate-color': cateItem.color,
|
||||||
|
}"
|
||||||
|
@click="toggleShowCate(cateItem.id)"
|
||||||
|
@touchstart="handleTouchStart(cateItem.id)"
|
||||||
|
@touchend="handleTouchEnd(cateItem.id)"
|
||||||
|
@touchmove="handleTouchMove(cateItem.id)"
|
||||||
|
>
|
||||||
|
<span class="cate-legend" />
|
||||||
|
<span
|
||||||
|
class="cate-name"
|
||||||
|
>
|
||||||
|
{{ cateItem.name }}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-if="cateItem.avg !== 0"
|
||||||
|
class="cate-avg-ms"
|
||||||
|
>
|
||||||
|
{{ cateItem.avg }}ms
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</popover>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<line-chart
|
||||||
|
:date-list="monitorChartData.dateList"
|
||||||
|
:value-list="monitorChartData.valueList"
|
||||||
|
:connect-nulls="false"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</dot-dot-box>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
@ -45,13 +194,16 @@ import {
|
|||||||
onMounted,
|
onMounted,
|
||||||
onUnmounted,
|
onUnmounted,
|
||||||
} from 'vue';
|
} from 'vue';
|
||||||
|
import { useStore } from 'vuex';
|
||||||
import config from '@/config';
|
import config from '@/config';
|
||||||
import request from '@/utils/request';
|
import request from '@/utils/request';
|
||||||
|
import validate from '@/utils/validate';
|
||||||
|
|
||||||
import LineChart from '@/components/charts/line.vue';
|
import LineChart from '@/components/charts/line.vue';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
getThreshold,
|
getThreshold,
|
||||||
|
getLineColor,
|
||||||
} from '@/views/composable/server-monitor';
|
} from '@/views/composable/server-monitor';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@ -61,80 +213,311 @@ const props = defineProps({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const peakShaving = ref(false);
|
const store = useStore();
|
||||||
|
|
||||||
|
const minute = ref(1440);
|
||||||
|
const minutes = [{
|
||||||
|
label: '30分钟',
|
||||||
|
value: 30,
|
||||||
|
}, {
|
||||||
|
label: '1小时',
|
||||||
|
value: 60,
|
||||||
|
}, {
|
||||||
|
label: '3小时',
|
||||||
|
value: 180,
|
||||||
|
}, {
|
||||||
|
label: '6小时',
|
||||||
|
value: 360,
|
||||||
|
}, {
|
||||||
|
label: '12小时',
|
||||||
|
value: 720,
|
||||||
|
}, {
|
||||||
|
label: '24小时',
|
||||||
|
value: 1440,
|
||||||
|
}];
|
||||||
|
const localData = {
|
||||||
|
peakShaving: window.localStorage.getItem('nazhua_monitor_peak_shaving'),
|
||||||
|
refreshData: window.localStorage.getItem('nazhua_monitor_refresh_data'),
|
||||||
|
chartType: window.localStorage.getItem('nazhua_monitor_chart_type'),
|
||||||
|
};
|
||||||
|
localData.peakShaving = validate.isSet(localData.peakShaving) ? localData.peakShaving === 'true' : false;
|
||||||
|
localData.refreshData = validate.isSet(localData.refreshData) ? localData.refreshData === 'true' : true;
|
||||||
|
|
||||||
|
const peakShaving = ref(localData.peakShaving);
|
||||||
|
const refreshData = ref(localData.refreshData);
|
||||||
|
const showCates = ref({});
|
||||||
const monitorData = ref([]);
|
const monitorData = ref([]);
|
||||||
|
const longPressTimer = ref(null);
|
||||||
|
|
||||||
|
const chartType = validate.isSet(localData.chartType)
|
||||||
|
? ref(localData.chartType)
|
||||||
|
: ref(config.nazhua.monitorChartType === 'single' ? 'single' : 'multi');
|
||||||
|
const monitorChartType = computed(() => {
|
||||||
|
if (config.nazhua.monitorChartTypeToggle) {
|
||||||
|
return chartType.value;
|
||||||
|
}
|
||||||
|
return config.nazhua.monitorChartType;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 服务器时间(后面来自接口)
|
||||||
|
const nowServerTime = computed(() => store.state.serverTime || Date.now());
|
||||||
|
// const nowServerTime = computed(() => Date.now());
|
||||||
|
// console.log(store.state.serverTime);
|
||||||
|
const acceptShowTime = computed(() => (Math.floor(nowServerTime.value / 60000) - minute.value) * 60000);
|
||||||
|
|
||||||
|
const minuteActiveArrowStyle = computed(() => {
|
||||||
|
const index = minutes.findIndex((i) => i.value === minute.value);
|
||||||
|
return {
|
||||||
|
left: `calc(${index} * var(--minute-item-width))`,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
const monitorChartData = computed(() => {
|
const monitorChartData = computed(() => {
|
||||||
const cateMap = {};
|
/**
|
||||||
const dateMap = {};
|
* 处理监控数据以生成分类的平均延迟随时间变化的列表。
|
||||||
monitorData.value.forEach((i) => {
|
*
|
||||||
if (!cateMap[i.monitor_name]) {
|
* @returns {Object} 返回一个对象,包含:
|
||||||
cateMap[i.monitor_name] = [];
|
* - cateList {Array}: 唯一监控名称的列表。
|
||||||
}
|
* - dateList {Array}: 排序后的唯一时间戳列表。
|
||||||
const {
|
* - valueList {Array}: 包含以下内容的对象列表:
|
||||||
threshold,
|
* - name {String}: 监控名称。
|
||||||
mean,
|
* - data {Array}: [时间戳, 平均延迟] 对的数组。
|
||||||
max,
|
*/
|
||||||
min,
|
|
||||||
} = peakShaving.value ? getThreshold(i.avg_delay, 2) : {};
|
|
||||||
i.created_at.forEach((o, index) => {
|
|
||||||
if (!dateMap[o]) {
|
|
||||||
dateMap[o] = [];
|
|
||||||
}
|
|
||||||
const avgDelay = i.avg_delay[index];
|
|
||||||
if (peakShaving.value) {
|
|
||||||
if (Math.abs(avgDelay - mean) > threshold && max / min > 2) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (avgDelay === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
dateMap[o].push({
|
|
||||||
name: i.monitor_name,
|
|
||||||
value: (avgDelay).toFixed(2) * 1,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
const dateList = [];
|
|
||||||
Object.keys(dateMap).forEach((i) => {
|
|
||||||
if (dateMap[i]?.length) {
|
|
||||||
dateList.push(parseInt(i, 10));
|
|
||||||
dateMap[i].forEach((o) => {
|
|
||||||
cateMap[o.name].push(o.value);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
dateList.sort((a, b) => a - b);
|
|
||||||
const cateList = [];
|
const cateList = [];
|
||||||
const valueList = [];
|
const cateMap = {};
|
||||||
Object.keys(cateMap).forEach((i) => {
|
const dateSet = new Set();
|
||||||
if (cateMap[i]?.length) {
|
let valueList = [];
|
||||||
cateList.push(i);
|
monitorData.value.forEach((i) => {
|
||||||
|
const dateMap = new Map();
|
||||||
|
const {
|
||||||
|
monitor_name,
|
||||||
|
monitor_id,
|
||||||
|
created_at,
|
||||||
|
avg_delay,
|
||||||
|
} = i;
|
||||||
|
if (!cateMap[monitor_name]) {
|
||||||
|
cateMap[monitor_name] = {
|
||||||
|
id: monitor_id,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
const cateDelayMap = new Map();
|
||||||
|
const cateAcceptTimeMap = new Map();
|
||||||
|
const cateCreateTime = new Set();
|
||||||
|
|
||||||
|
// 实际数据的最早时间戳
|
||||||
|
let earliestTimestamp = nowServerTime.value;
|
||||||
|
created_at.forEach((time, index) => {
|
||||||
|
if (time < earliestTimestamp) {
|
||||||
|
earliestTimestamp = time;
|
||||||
|
}
|
||||||
|
const status = time >= acceptShowTime.value;
|
||||||
|
|
||||||
|
// 允许显示的数据,记录到cateAcceptTime
|
||||||
|
if (status) {
|
||||||
|
if (import.meta.env.VITE_MONITOR_DEBUG === '1' && cateAcceptTimeMap.has(time)) {
|
||||||
|
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') {
|
||||||
|
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) {
|
||||||
|
const time = actualStartTime + j * 60000;
|
||||||
|
// 记录创建时间
|
||||||
|
cateCreateTime.add(time);
|
||||||
|
// 记录延迟数据
|
||||||
|
const timeProp = cateAcceptTimeMap.get(time);
|
||||||
|
cateDelayMap.set(time, timeProp ?? undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算削峰阈值
|
||||||
|
const {
|
||||||
|
median,
|
||||||
|
tolerancePercent,
|
||||||
|
} = peakShaving.value ? getThreshold(Array.from(cateDelayMap.values())) : {};
|
||||||
|
|
||||||
|
// 合成分钟数据
|
||||||
|
cateCreateTime.values().forEach((time) => {
|
||||||
|
const avgDelay = cateDelayMap.get(time) * 1;
|
||||||
|
|
||||||
|
// 只对有效的延迟值进行削峰判断
|
||||||
|
if (peakShaving.value) {
|
||||||
|
// 削峰过滤:根据中位数和动态容差百分比判断异常值
|
||||||
|
const threshold = median * tolerancePercent;
|
||||||
|
// 当偏离中位数超过阈值时,视为异常值
|
||||||
|
if (Math.abs(avgDelay - median) > threshold) {
|
||||||
|
dateMap.set(time, null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 无数据或无效数据的情况,设置为undefined
|
||||||
|
if (Number.isNaN(avgDelay)) {
|
||||||
|
dateMap.set(time, undefined);
|
||||||
|
} else {
|
||||||
|
dateMap.set(time, (avgDelay).toFixed(2) * 1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const lineData = [];
|
||||||
|
const validatedData = [];
|
||||||
|
const overValidatedData = [];
|
||||||
|
let delayTotal = 0;
|
||||||
|
dateMap.forEach((val, key) => {
|
||||||
|
const time = parseInt(key, 10); // 时间戳
|
||||||
|
lineData.push([time, val || null]);
|
||||||
|
if (val) {
|
||||||
|
dateSet.add(time);
|
||||||
|
validatedData.push([time, val]);
|
||||||
|
delayTotal += val;
|
||||||
|
}
|
||||||
|
if (val !== undefined) {
|
||||||
|
overValidatedData.push([time, val]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (import.meta.env.VITE_MONITOR_DEBUG === '1') {
|
||||||
|
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)) {
|
||||||
|
showCates.value[id] = true;
|
||||||
|
}
|
||||||
|
const color = getLineColor(id);
|
||||||
|
// 成功率 = 有效数据点 / 所有数据点
|
||||||
|
const over = overValidatedData.length > 0 ? overValidatedData.length / lineData.length : 0;
|
||||||
|
const validRate = 1 - ((validatedData.length > 0 && overValidatedData.length > 0)
|
||||||
|
? validatedData.length / overValidatedData.length : 0);
|
||||||
|
const cateItem = {
|
||||||
|
id,
|
||||||
|
name: monitor_name,
|
||||||
|
color,
|
||||||
|
avg: avgDelay.toFixed(2) * 1,
|
||||||
|
over: (over * 100).toFixed(2) * 1,
|
||||||
|
validRate: (validRate * 100).toFixed(2) * 1,
|
||||||
|
};
|
||||||
|
const titles = [
|
||||||
|
cateItem.name,
|
||||||
|
cateItem.avg === 0 ? '' : `平均延迟:${cateItem.avg}ms`,
|
||||||
|
`成功率:${cateItem.over}%`,
|
||||||
|
];
|
||||||
|
if (peakShaving.value) {
|
||||||
|
titles.push(`削峰率: ${cateItem.validRate}%`);
|
||||||
|
}
|
||||||
|
cateItem.title = titles.filter((s) => s).join('\n');
|
||||||
|
cateList.push(cateItem);
|
||||||
valueList.push({
|
valueList.push({
|
||||||
name: i,
|
id,
|
||||||
data: cateMap[i],
|
name: monitor_name,
|
||||||
|
data: lineData,
|
||||||
|
itemStyle: {
|
||||||
|
color,
|
||||||
|
},
|
||||||
|
lineStyle: {
|
||||||
|
color,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const dateList = Array.from(dateSet).sort((a, b) => a - b);
|
||||||
|
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 {
|
||||||
cateList,
|
|
||||||
dateList,
|
dateList,
|
||||||
|
cateList,
|
||||||
valueList,
|
valueList,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
function switchPeakShaving() {
|
function switchPeakShaving() {
|
||||||
peakShaving.value = !peakShaving.value;
|
peakShaving.value = !peakShaving.value;
|
||||||
|
window.localStorage.setItem('nazhua_monitor_peak_shaving', peakShaving.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function switchRefresh() {
|
||||||
|
refreshData.value = !refreshData.value;
|
||||||
|
window.localStorage.setItem('nazhua_monitor_refresh_data', refreshData.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function switchChartType() {
|
||||||
|
chartType.value = chartType.value === 'single' ? 'multi' : 'single';
|
||||||
|
window.localStorage.setItem('nazhua_monitor_chart_type', chartType.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleMinute(value) {
|
||||||
|
minute.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleShowCate(id) {
|
||||||
|
if (window.innerWidth < 768) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
showCates.value[id] = !showCates.value[id];
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleTouchStart(id) {
|
||||||
|
longPressTimer.value = setTimeout(() => {
|
||||||
|
showCates.value[id] = !showCates.value[id];
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleTouchEnd() {
|
||||||
|
if (longPressTimer.value) {
|
||||||
|
clearTimeout(longPressTimer.value);
|
||||||
|
longPressTimer.value = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleTouchMove() {
|
||||||
|
if (longPressTimer.value) {
|
||||||
|
clearTimeout(longPressTimer.value);
|
||||||
|
longPressTimer.value = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadMonitor() {
|
async function loadMonitor() {
|
||||||
await request({
|
await request({
|
||||||
url: config.nazhua.apiMonitorPath.replace('{id}', props.info.ID),
|
url: (
|
||||||
|
config.nazhua.nezhaVersion === 'v1' ? config.nazhua.v1ApiMonitorPath : config.nazhua.apiMonitorPath
|
||||||
|
).replace('{id}', props.info.ID),
|
||||||
}).then((res) => {
|
}).then((res) => {
|
||||||
if (Array.isArray(res)) {
|
const list = config.nazhua.nezhaVersion === 'v1' ? res.data?.data : res.data?.result;
|
||||||
monitorData.value = res;
|
if (Array.isArray(list)) {
|
||||||
|
monitorData.value = list;
|
||||||
}
|
}
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
@ -142,18 +525,31 @@ async function loadMonitor() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let loadMonitorTimer = null;
|
let loadMonitorTimer = null;
|
||||||
async function setTimeLoadMonitor() {
|
async function setTimeLoadMonitor(force = false) {
|
||||||
if (loadMonitorTimer) {
|
if (loadMonitorTimer) {
|
||||||
clearTimeout(loadMonitorTimer);
|
clearTimeout(loadMonitorTimer);
|
||||||
}
|
}
|
||||||
|
if (refreshData.value || force) {
|
||||||
await loadMonitor();
|
await loadMonitor();
|
||||||
|
}
|
||||||
|
let monitorRefreshTime = parseInt(config.nazhua.monitorRefreshTime, 10);
|
||||||
|
// 0 为不刷新
|
||||||
|
if (monitorRefreshTime === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 非数字 强制为30
|
||||||
|
if (Number.isNaN(monitorRefreshTime)) {
|
||||||
|
monitorRefreshTime = 30;
|
||||||
|
}
|
||||||
|
// 最小 10 秒
|
||||||
|
const sTime = Math.min(monitorRefreshTime, 10);
|
||||||
loadMonitorTimer = setTimeout(() => {
|
loadMonitorTimer = setTimeout(() => {
|
||||||
setTimeLoadMonitor();
|
setTimeLoadMonitor();
|
||||||
}, 10000);
|
}, sTime * 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
setTimeLoadMonitor();
|
setTimeLoadMonitor(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
@ -165,39 +561,99 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.server-monitor-group {
|
.server-monitor-group {
|
||||||
padding: 16px 20px;
|
--line-chart-size: 300px;
|
||||||
border-radius: 12px;
|
|
||||||
background-image: radial-gradient(transparent 1px, rgba(#000, 0.6) 1px);
|
|
||||||
background-size: 3px 3px;
|
|
||||||
backdrop-filter: saturate(50%) blur(3px);
|
|
||||||
box-shadow: 2px 4px 6px rgba(#000, 0.4);
|
|
||||||
|
|
||||||
--line-chart-size: 270px;
|
&.chart-type--single {
|
||||||
|
--line-chart-size: 240px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.monitor-cate-item {
|
||||||
|
--cate-item-height: 28px;
|
||||||
|
--cate-item-font-size: 14px;
|
||||||
|
--cate-color: #fff;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
width: var(--cate-item-width);
|
||||||
|
height: var(--cate-item-height);
|
||||||
|
gap: 6px;
|
||||||
|
padding: 0 6px;
|
||||||
|
font-size: var(--cate-item-font-size);
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
@media screen and (max-width: 768px) {
|
@media screen and (max-width: 768px) {
|
||||||
background-color: rgba(#000, 0.8);
|
cursor: default;
|
||||||
background-image: none;
|
}
|
||||||
backdrop-filter: none;
|
|
||||||
|
.cate-legend {
|
||||||
|
width: 0.5em;
|
||||||
|
height: 0.5em;
|
||||||
|
background: var(--cate-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cate-name {
|
||||||
|
// flex: 1;
|
||||||
|
height: var(--cate-item-height);
|
||||||
|
line-height: calc(var(--cate-item-height) + 2px);
|
||||||
|
color: #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cate-avg-ms {
|
||||||
|
height: var(--cate-item-height);
|
||||||
|
line-height: calc(var(--cate-item-height) + 2px);
|
||||||
|
text-align: right;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cate-over-rate {
|
||||||
|
height: var(--cate-item-height);
|
||||||
|
line-height: calc(var(--cate-item-height) + 2px);
|
||||||
|
text-align: right;
|
||||||
|
color: #fffbd8;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.disabled {
|
||||||
|
filter: grayscale(1) brightness(0.8);
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.module-head-group {
|
.module-head-group {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
height: 30px;
|
|
||||||
|
@media screen and (min-width: 768px) {
|
||||||
|
position: sticky;
|
||||||
|
top: var(--layout-header-height);
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
.module-title {
|
.module-title {
|
||||||
|
width: max-content;
|
||||||
|
height: 30px;
|
||||||
line-height: 30px;
|
line-height: 30px;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
color: #eee;
|
color: #eee;
|
||||||
}
|
}
|
||||||
|
|
||||||
.peak-shaving-group {
|
.right-box {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.peak-shaving-group,
|
||||||
|
.refresh-data-group,
|
||||||
|
.chart-type-switch-group {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6px;
|
gap: 4px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
@media screen and (max-width: 1024px) {
|
@media screen and (max-width: 1024px) {
|
||||||
@ -228,6 +684,7 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
.switch-dot {
|
.switch-dot {
|
||||||
left: 16px;
|
left: 16px;
|
||||||
|
box-shadow: 1px 1px 2px rgba(#000, 0.4);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -237,5 +694,114 @@ onUnmounted(() => {
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.last-update-time-group {
|
||||||
|
--minute-item-width: 50px;
|
||||||
|
--minute-item-height: 20px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
|
||||||
|
.last-update-time-label {
|
||||||
|
color: #ddd;
|
||||||
|
height: var(--minute-item-height);
|
||||||
|
line-height: var(--minute-item-height);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 660px) {
|
||||||
|
--minute-item-width: 46px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 600px) {
|
||||||
|
--minute-item-width: 46px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 400px) {
|
||||||
|
.last-update-time-label {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 330px) {
|
||||||
|
margin-left: -12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 320px) {
|
||||||
|
margin-left: -18px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.minutes {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
// padding: 0 10px;
|
||||||
|
height: var(--minute-item-height);
|
||||||
|
background: rgba(#fff, 0.2);
|
||||||
|
border-radius: calc(var(--minute-item-height) / 2);
|
||||||
|
|
||||||
|
.minute-item {
|
||||||
|
position: relative;
|
||||||
|
z-index: 10;
|
||||||
|
width: var(--minute-item-width);
|
||||||
|
height: var(--minute-item-height);
|
||||||
|
line-height: var(--minute-item-height);
|
||||||
|
font-size: 12px;
|
||||||
|
text-align: center;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #aaa;
|
||||||
|
transition: color 0.3s;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
color: #fff;
|
||||||
|
text-shadow: 1px 1px 2px rgba(#000, 0.6);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.active-arrow {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: var(--minute-item-width);
|
||||||
|
height: var(--minute-item-height);
|
||||||
|
border-radius: calc(var(--minute-item-height) / 2);
|
||||||
|
background: #4caf50;
|
||||||
|
// opacity: 0.5;
|
||||||
|
transition: left 0.3s;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.monitor-cate-group {
|
||||||
|
--gap-size: 0;
|
||||||
|
margin: 10px 0;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: var(--gap-size);
|
||||||
|
margin-right: calc(var(--gap-size) * -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.monitor-chart-group {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px 0;
|
||||||
|
|
||||||
|
.monitor-chart-item {
|
||||||
|
width: 50%;
|
||||||
|
height: calc(var(--line-chart-size) + 28px);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 768px) {
|
||||||
|
.monitor-chart-item {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.cate-name-box {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -1,12 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="server-head">
|
<dot-dot-box
|
||||||
<div class="server-flag">
|
class="server-head"
|
||||||
<div class="server-flag-font">
|
padding="16px"
|
||||||
<span
|
>
|
||||||
class="fi"
|
<div class="server-flag-box">
|
||||||
:class="'fi-' + (info?.Host?.CountryCode || 'un')"
|
<server-flag :info="info" />
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="server-name-and-slogan">
|
<div class="server-name-and-slogan">
|
||||||
<div class="server-name-group">
|
<div class="server-name-group">
|
||||||
@ -18,12 +16,9 @@
|
|||||||
class="cpu-mem-group"
|
class="cpu-mem-group"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
v-if="info?.Host?.Platform"
|
|
||||||
class="system-os-icon"
|
class="system-os-icon"
|
||||||
>
|
>
|
||||||
<span
|
<span :class="platformLogoIconClassName" />
|
||||||
:class="'fl-' + info?.Host?.Platform"
|
|
||||||
/>
|
|
||||||
</span>
|
</span>
|
||||||
<span class="core-mem">{{ cpuAndMemAndDisk }}</span>
|
<span class="core-mem">{{ cpuAndMemAndDisk }}</span>
|
||||||
</span>
|
</span>
|
||||||
@ -59,7 +54,7 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</dot-dot-box>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
@ -87,22 +82,17 @@ const { cpuAndMemAndDisk } = handleServerInfo({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const slogan = computed(() => props.info?.PublicNote?.customData?.slogan);
|
const slogan = computed(() => props.info?.PublicNote?.customData?.slogan);
|
||||||
const cpuInfo = computed(() => hostUtils.getCPUInfo(props.info.Host.CPU[0]));
|
const cpuInfo = computed(() => hostUtils.getCPUInfo(props.info?.Host?.CPU?.[0]));
|
||||||
|
const platformLogoIconClassName = computed(() => hostUtils.getPlatformLogoIconClassName(props.info?.Host?.Platform));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.server-head {
|
.server-head {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
padding: 16px;
|
|
||||||
border-radius: 12px;
|
|
||||||
background-image: radial-gradient(transparent 1px, rgba(#000, 0.8) 1px);
|
|
||||||
background-size: 3px 3px;
|
|
||||||
backdrop-filter: saturate(50%) blur(3px);
|
|
||||||
transition: 0.3s;
|
transition: 0.3s;
|
||||||
box-shadow: 2px 4px 6px rgba(#000, 0.4);
|
|
||||||
|
|
||||||
.server-flag {
|
.server-flag-box {
|
||||||
--flag-size: 72px;
|
--flag-size: 72px;
|
||||||
position: relative;
|
position: relative;
|
||||||
width: calc(var(--flag-size) * 1.33333333);
|
width: calc(var(--flag-size) * 1.33333333);
|
||||||
@ -110,7 +100,7 @@ const cpuInfo = computed(() => hostUtils.getCPUInfo(props.info.Host.CPU[0]));
|
|||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
.server-flag-font {
|
.server-flag {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
@ -149,6 +139,7 @@ const cpuInfo = computed(() => hostUtils.getCPUInfo(props.info.Host.CPU[0]));
|
|||||||
.server-name {
|
.server-name {
|
||||||
line-height: 30px;
|
line-height: 30px;
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
|
font-weight: bold;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -161,6 +152,7 @@ const cpuInfo = computed(() => hostUtils.getCPUInfo(props.info.Host.CPU[0]));
|
|||||||
.core-mem {
|
.core-mem {
|
||||||
line-height: 30px;
|
line-height: 30px;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 500px) {
|
@media screen and (max-width: 500px) {
|
||||||
@ -214,10 +206,17 @@ const cpuInfo = computed(() => hostUtils.getCPUInfo(props.info.Host.CPU[0]));
|
|||||||
font-family: Arial, "Helvetica Neue", Helvetica, sans-serif;
|
font-family: Arial, "Helvetica Neue", Helvetica, sans-serif;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
&--amd {
|
&--amd {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
font-family: Arial, "Helvetica Neue", Helvetica, sans-serif;
|
font-family: Arial, "Helvetica Neue", Helvetica, sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&--apple {
|
||||||
|
font-weight: 600;
|
||||||
|
font-family: PingFang SC, Arial, "Helvetica Neue", Helvetica, sans-serif;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.cpu-model {
|
.cpu-model {
|
||||||
@ -227,6 +226,17 @@ const cpuInfo = computed(() => hostUtils.getCPUInfo(props.info.Host.CPU[0]));
|
|||||||
.cpu-model-num {
|
.cpu-model-num {
|
||||||
color: #c7eeff;
|
color: #c7eeff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 500px) {
|
||||||
|
padding-left: 0;
|
||||||
|
margin-top: -7px;
|
||||||
|
line-height: 16px;
|
||||||
|
.cpu-company {
|
||||||
|
height: 16px;
|
||||||
|
line-height: 16px;
|
||||||
|
padding: 0 3px;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -1,5 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="server-status-and-real-time">
|
<dot-dot-box
|
||||||
|
padding="15px"
|
||||||
|
class="server-status-and-real-time"
|
||||||
|
:class="{
|
||||||
|
'status-type--progress': componentName === 'progress',
|
||||||
|
}"
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
class="server-status-group"
|
class="server-status-group"
|
||||||
:class="'type--' + componentName + ' status-list--' + serverStatusList.length"
|
:class="'type--' + componentName + ' status-list--' + serverStatusList.length"
|
||||||
@ -17,7 +23,7 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<server-list-item-real-time :info="info" />
|
<server-list-item-real-time :info="info" />
|
||||||
</div>
|
</dot-dot-box>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
@ -64,25 +70,23 @@ const {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
padding: 15px;
|
|
||||||
border-radius: 12px;
|
|
||||||
background-image: radial-gradient(transparent 1px, rgba(#000, 0.6) 1px);
|
|
||||||
background-size: 3px 3px;
|
|
||||||
backdrop-filter: saturate(50%) blur(3px);
|
|
||||||
box-shadow: 2px 4px 6px rgba(#000, 0.4);
|
|
||||||
|
|
||||||
--real-time-value-font-size: 36px;
|
--real-time-value-font-size: 36px;
|
||||||
--real-time-text-font-size: 16px;
|
--real-time-text-font-size: 16px;
|
||||||
--real-time-label-font-size: 16px;
|
--real-time-label-font-size: 16px;
|
||||||
|
|
||||||
|
&.status-type--progress {
|
||||||
|
--real-time-value-font-size: 24px;
|
||||||
|
--real-time-text-font-size: 14px;
|
||||||
|
--real-time-label-font-size: 14px;
|
||||||
|
|
||||||
@media screen and (max-width: 1024px) {
|
@media screen and (max-width: 1024px) {
|
||||||
--real-time-value-font-size: 30px;
|
--real-time-value-font-size: 24px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 768px) {
|
@media screen and (max-width: 1024px) {
|
||||||
background-color: rgba(#000, 0.8);
|
--real-time-value-font-size: 30px;
|
||||||
background-image: none;
|
|
||||||
backdrop-filter: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 720px) {
|
@media screen and (max-width: 720px) {
|
||||||
|
|||||||
@ -2,6 +2,9 @@
|
|||||||
<div
|
<div
|
||||||
v-if="show"
|
v-if="show"
|
||||||
class="server-list-item-bill"
|
class="server-list-item-bill"
|
||||||
|
:class="{
|
||||||
|
'dot-dot-box--hide': $config.nazhua?.hideDotBG === true,
|
||||||
|
}"
|
||||||
>
|
>
|
||||||
<div class="left-box">
|
<div class="left-box">
|
||||||
<div
|
<div
|
||||||
@ -33,6 +36,9 @@
|
|||||||
v-for="(tagItem, index) in tagList"
|
v-for="(tagItem, index) in tagList"
|
||||||
:key="`${tagItem}_${index}`"
|
:key="`${tagItem}_${index}`"
|
||||||
class="tag-item"
|
class="tag-item"
|
||||||
|
:class="{
|
||||||
|
'has-sarasa-term': $hasSarasaTerm && config.nazhua.disableSarasaTermSC !== true,
|
||||||
|
}"
|
||||||
>
|
>
|
||||||
{{ tagItem }}
|
{{ tagItem }}
|
||||||
</span>
|
</span>
|
||||||
@ -45,8 +51,10 @@
|
|||||||
>
|
>
|
||||||
<span class="text">
|
<span class="text">
|
||||||
<span class="text-item value-text">{{ billAndPlan.billing.value }}</span>
|
<span class="text-item value-text">{{ billAndPlan.billing.value }}</span>
|
||||||
|
<template v-if="!billAndPlan.billing.isFree && billAndPlan.billing.cycleLabel">
|
||||||
<span class="text-item">/</span>
|
<span class="text-item">/</span>
|
||||||
<span class="text-item label-text">{{ billAndPlan.billing.cycleLabel }}</span>
|
<span class="text-item label-text">{{ billAndPlan.billing.cycleLabel }}</span>
|
||||||
|
</template>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@ -55,7 +63,7 @@
|
|||||||
@click.stop="toBuy"
|
@click.stop="toBuy"
|
||||||
>
|
>
|
||||||
<span class="icon">
|
<span class="icon">
|
||||||
<span class="ri-shopping-bag-3-line" />
|
<span :class="buyBtnIcon" />
|
||||||
</span>
|
</span>
|
||||||
<span class="text">{{ buyBtnText }}</span>
|
<span class="text">{{ buyBtnText }}</span>
|
||||||
</div>
|
</div>
|
||||||
@ -88,8 +96,24 @@ const {
|
|||||||
props,
|
props,
|
||||||
});
|
});
|
||||||
|
|
||||||
const buyBtnText = config.nazhua.buyBtnText || '购买';
|
const buyBtnIcon = computed(() => {
|
||||||
const showBuyBtn = computed(() => !!props.info?.PublicNote?.customData?.orderLink);
|
if (props.info?.PublicNote?.customData?.buyBtnIcon) {
|
||||||
|
return props.info?.PublicNote?.customData?.buyBtnIcon;
|
||||||
|
}
|
||||||
|
return config.nazhua.buyBtnIcon || 'ri-shopping-bag-3-line';
|
||||||
|
});
|
||||||
|
const buyBtnText = computed(() => {
|
||||||
|
if (props.info?.PublicNote?.customData?.buyBtnText) {
|
||||||
|
return props.info?.PublicNote?.customData?.buyBtnText;
|
||||||
|
}
|
||||||
|
return config.nazhua.buyBtnText || '购买';
|
||||||
|
});
|
||||||
|
const showBuyBtn = computed(() => {
|
||||||
|
if (config.nazhua.hideListItemLink === true) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return !!props.info?.PublicNote?.customData?.orderLink;
|
||||||
|
});
|
||||||
|
|
||||||
function toBuy() {
|
function toBuy() {
|
||||||
const decodeUrl = decodeURIComponent(props.info?.PublicNote?.customData?.orderLink);
|
const decodeUrl = decodeURIComponent(props.info?.PublicNote?.customData?.orderLink);
|
||||||
@ -98,18 +122,33 @@ function toBuy() {
|
|||||||
|
|
||||||
const tagList = computed(() => {
|
const tagList = computed(() => {
|
||||||
const list = [];
|
const list = [];
|
||||||
if (props?.info?.PublicNote?.planDataMod?.networkRoute) {
|
const {
|
||||||
list.push(...props.info.PublicNote.planDataMod.networkRoute.split(','));
|
networkRoute,
|
||||||
|
extra,
|
||||||
|
IPv4,
|
||||||
|
IPv6,
|
||||||
|
} = props?.info?.PublicNote?.planDataMod || {};
|
||||||
|
if (networkRoute) {
|
||||||
|
list.push(...networkRoute.split(','));
|
||||||
}
|
}
|
||||||
if (props?.info?.PublicNote?.planDataMod?.extra) {
|
if (extra) {
|
||||||
list.push(...props.info.PublicNote.planDataMod.extra.split(','));
|
list.push(...extra.split(','));
|
||||||
}
|
}
|
||||||
return list;
|
if (IPv4 === '1' && IPv6 === '1') {
|
||||||
|
list.push('双栈IP');
|
||||||
|
} else if (IPv4 === '1') {
|
||||||
|
list.push('仅IPv4');
|
||||||
|
} else if (IPv6 === '1') {
|
||||||
|
list.push('仅IPv6');
|
||||||
|
}
|
||||||
|
// 列表最多显示5个标签
|
||||||
|
return list.slice(0, 5);
|
||||||
});
|
});
|
||||||
|
|
||||||
const show = computed(() => {
|
const show = computed(() => {
|
||||||
const checks = [
|
const checks = [
|
||||||
billAndPlan.value.remainingTime,
|
billAndPlan.value.remainingTime,
|
||||||
|
billAndPlan.value.billing,
|
||||||
tagList.value.length > 0,
|
tagList.value.length > 0,
|
||||||
showBuyBtn.value,
|
showBuyBtn.value,
|
||||||
];
|
];
|
||||||
@ -123,12 +162,29 @@ 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 {
|
||||||
|
box-shadow: none;
|
||||||
|
border-top: 1px solid rgba(#ddd, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
.left-box {
|
.left-box {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
@ -142,23 +198,27 @@ const show = computed(() => {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 30px;
|
width: calc(var(--list-item-bill-height) * 0.75);
|
||||||
height: 30px;
|
height: calc(var(--list-item-bill-height) * 0.75);
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
font-size: 16px;
|
font-size: var(--list-item-bill-icon-font-size);
|
||||||
color: #74dbef;
|
color: #74dbef;
|
||||||
}
|
}
|
||||||
|
|
||||||
.text {
|
.text {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
line-height: 30px;
|
line-height: var(--list-item-bill-height);
|
||||||
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 {
|
||||||
@ -172,11 +232,16 @@ const show = computed(() => {
|
|||||||
.tag-item {
|
.tag-item {
|
||||||
height: 18px;
|
height: 18px;
|
||||||
padding: 0 4px;
|
padding: 0 4px;
|
||||||
line-height: 20px;
|
line-height: 18px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: var(--public-note-tag-color);
|
color: var(--public-note-tag-color);
|
||||||
background-color: var(--public-note-tag-bg);
|
background: var(--public-note-tag-bg);
|
||||||
|
text-shadow: 1px 1px 2px rgba(#000, 0.2);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
|
||||||
|
&.has-sarasa-term {
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="server-list-item-status"
|
class="server-list-item-status"
|
||||||
:class="'type--' + componentName"
|
:class="classNames"
|
||||||
>
|
>
|
||||||
<component
|
<component
|
||||||
:is="componentMaps[componentName]"
|
:is="componentMaps[componentName]"
|
||||||
@ -21,6 +21,10 @@
|
|||||||
* 服务器状态盒子
|
* 服务器状态盒子
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
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';
|
||||||
@ -39,10 +43,13 @@ const componentMaps = {
|
|||||||
progress: ServerStatusProgress,
|
progress: ServerStatusProgress,
|
||||||
};
|
};
|
||||||
|
|
||||||
const componentName = [
|
const componentName = computed(() => {
|
||||||
|
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,
|
||||||
@ -51,6 +58,13 @@ 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>
|
||||||
@ -63,11 +77,16 @@ const {
|
|||||||
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: 350px) {
|
@media screen and (max-width: 400px) {
|
||||||
--progress-bar-height: 16px;
|
--progress-bar-height: 16px;
|
||||||
padding: 0 15px;
|
padding: 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.len--3 {
|
||||||
|
--progress-bar-width: calc((100% - 20px) / 3);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1,5 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<dot-dot-box
|
||||||
|
border-radius="var(--list-item-border-radius)"
|
||||||
|
:padding="0"
|
||||||
class="server-list-item"
|
class="server-list-item"
|
||||||
:class="{
|
:class="{
|
||||||
'server-list-item--offline': info.online === -1,
|
'server-list-item--offline': info.online === -1,
|
||||||
@ -7,17 +9,13 @@
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="server-info-group server-list-item-head"
|
class="server-info-group server-list-item-head"
|
||||||
|
:class="{
|
||||||
|
'dot-dot-box--hide': $config.nazhua?.hideDotBG === true,
|
||||||
|
}"
|
||||||
@click="openDetail"
|
@click="openDetail"
|
||||||
>
|
>
|
||||||
<div class="server-name-group left-box">
|
<div class="server-name-group left-box">
|
||||||
<span
|
<server-flag :info="info" />
|
||||||
class="server-flag"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="fi"
|
|
||||||
:class="'fi-' + (info?.Host?.CountryCode || 'un')"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
<span class="server-name">
|
<span class="server-name">
|
||||||
{{ info.Name }}
|
{{ info.Name }}
|
||||||
</span>
|
</span>
|
||||||
@ -27,33 +25,31 @@
|
|||||||
v-if="cpuAndMemAndDisk"
|
v-if="cpuAndMemAndDisk"
|
||||||
class="cpu-mem-group"
|
class="cpu-mem-group"
|
||||||
>
|
>
|
||||||
<span
|
<span :class="platformLogoIconClassName" />
|
||||||
v-if="info?.Host?.Platform"
|
|
||||||
:class="'fl-' + info?.Host?.Platform"
|
|
||||||
/>
|
|
||||||
<span class="core-mem">{{ cpuAndMemAndDisk }}</span>
|
<span class="core-mem">{{ cpuAndMemAndDisk }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="showStatus || showStatus"
|
v-if="$config.nazhua.hideListItemStatusDonut !== true && $config.nazhua.hideListItemStat !== true"
|
||||||
class="server-list-item-main"
|
class="server-list-item-main"
|
||||||
@click="openDetail"
|
@click="openDetail"
|
||||||
>
|
>
|
||||||
<server-list-item-status
|
<server-list-item-status
|
||||||
v-if="showStatus"
|
v-if="$config.nazhua.hideListItemStatusDonut !== true"
|
||||||
:info="info"
|
:info="info"
|
||||||
/>
|
/>
|
||||||
<server-real-time
|
<server-real-time
|
||||||
v-if="showStat"
|
v-if="$config.nazhua.hideListItemStat !== true"
|
||||||
:info="info"
|
:info="info"
|
||||||
|
:server-real-time-list-tpls="serverRealTimeListTpls"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<server-list-item-bill
|
<server-list-item-bill
|
||||||
v-if="showBill"
|
v-if="$config.nazhua.hideListItemBill !== true"
|
||||||
:info="info"
|
:info="info"
|
||||||
/>
|
/>
|
||||||
</div>
|
</dot-dot-box>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
@ -61,11 +57,14 @@
|
|||||||
* 单节点
|
* 单节点
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
computed,
|
||||||
|
} from 'vue';
|
||||||
import {
|
import {
|
||||||
useRouter,
|
useRouter,
|
||||||
} from 'vue-router';
|
} from 'vue-router';
|
||||||
|
|
||||||
import config from '@/config';
|
import config from '@/config';
|
||||||
|
import * as hostUtils from '@/utils/host';
|
||||||
|
|
||||||
import handleServerInfo from '@/views/composable/server-info';
|
import handleServerInfo from '@/views/composable/server-info';
|
||||||
import ServerRealTime from '@/views/components/server/server-real-time.vue';
|
import ServerRealTime from '@/views/components/server/server-real-time.vue';
|
||||||
@ -88,6 +87,15 @@ const { cpuAndMemAndDisk } = handleServerInfo({
|
|||||||
props,
|
props,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const platformLogoIconClassName = computed(() => hostUtils.getPlatformLogoIconClassName(props.info?.Host?.Platform));
|
||||||
|
|
||||||
|
const serverRealTimeListTpls = computed(() => {
|
||||||
|
if (config.nazhua?.listServerRealTimeShowLoad || config.nazhua.listServerItemType === 'server-status') {
|
||||||
|
return 'D-A-T,T-A-U,L-A-P,I-A-O';
|
||||||
|
}
|
||||||
|
return 'duration,transfer,inSpeed,outSpeed';
|
||||||
|
});
|
||||||
|
|
||||||
function openDetail() {
|
function openDetail() {
|
||||||
router.push({
|
router.push({
|
||||||
name: 'ServerDetail',
|
name: 'ServerDetail',
|
||||||
@ -96,10 +104,6 @@ function openDetail() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const showStatus = config.nazhua.hideListItemStatusDonut !== true;
|
|
||||||
const showStat = config.nazhua.hideListItemStat !== true;
|
|
||||||
const showBill = config.nazhua.hideListItemBill !== true;
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@ -107,26 +111,15 @@ const showBill = config.nazhua.hideListItemBill !== true;
|
|||||||
--list-item-border-radius: 12px;
|
--list-item-border-radius: 12px;
|
||||||
width: var(--list-item-width);
|
width: var(--list-item-width);
|
||||||
color: #fff;
|
color: #fff;
|
||||||
background-image: radial-gradient(transparent 1px, rgba(#000, 0.6) 1px);
|
|
||||||
background-size: 3px 3px;
|
|
||||||
backdrop-filter: saturate(50%) blur(3px);
|
|
||||||
border-radius: var(--list-item-border-radius);
|
|
||||||
transition: 0.3s;
|
transition: 0.3s;
|
||||||
box-shadow: 2px 4px 6px rgba(#000, 0.4);
|
|
||||||
|
|
||||||
@media screen and (max-width: 768px) {
|
|
||||||
background-color: rgba(#000, 0.8);
|
|
||||||
background-image: none;
|
|
||||||
backdrop-filter: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.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);
|
||||||
@ -135,11 +128,18 @@ const showBill = config.nazhua.hideListItemBill !== true;
|
|||||||
|
|
||||||
@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 {
|
||||||
|
box-shadow: none;
|
||||||
|
border-bottom: 1px solid rgba(#ddd, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.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,
|
||||||
@ -162,6 +162,7 @@ const showBill = config.nazhua.hideListItemBill !== true;
|
|||||||
height: 30px;
|
height: 30px;
|
||||||
line-height: 32px;
|
line-height: 32px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cpu-mem-group {
|
.cpu-mem-group {
|
||||||
@ -173,6 +174,7 @@ const showBill = config.nazhua.hideListItemBill !== true;
|
|||||||
.core-mem {
|
.core-mem {
|
||||||
height: 30px;
|
height: 30px;
|
||||||
line-height: 32px;
|
line-height: 32px;
|
||||||
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -196,6 +198,8 @@ const showBill = config.nazhua.hideListItemBill !== true;
|
|||||||
--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;
|
||||||
|
|
||||||
@ -210,8 +214,12 @@ const showBill = config.nazhua.hideListItemBill !== true;
|
|||||||
--real-time-value-font-size: 20px;
|
--real-time-value-font-size: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 680px) {
|
@media screen and (max-width: 720px) {
|
||||||
--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) {
|
||||||
238
src/views/components/server-list/row/server-list-column.vue
Normal file
238
src/views/components/server-list/row/server-list-column.vue
Normal file
@ -0,0 +1,238 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="list-column"
|
||||||
|
:class="`list-column--${prop}`"
|
||||||
|
:style="columnStyle"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
ref="columnContentRef"
|
||||||
|
class="list-column-content"
|
||||||
|
>
|
||||||
|
<span class="item-label">{{ label }}</span>
|
||||||
|
<div class="item-content">
|
||||||
|
<template v-if="slotContent">
|
||||||
|
<slot />
|
||||||
|
</template>
|
||||||
|
<template v-if="slotValue">
|
||||||
|
<span class="item-text item-value">
|
||||||
|
<slot name="value" />
|
||||||
|
</span>
|
||||||
|
<span class="item-text item-unit">
|
||||||
|
<slot name="unit" />
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<span class="item-text item-value">{{ value }}</span>
|
||||||
|
<span
|
||||||
|
v-if="unit"
|
||||||
|
class="item-text item-unit"
|
||||||
|
>{{ unit }}</span>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
/**
|
||||||
|
* 服务器信息列表列
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
computed,
|
||||||
|
ref,
|
||||||
|
onMounted,
|
||||||
|
onBeforeUnmount,
|
||||||
|
} from 'vue';
|
||||||
|
import {
|
||||||
|
useStore,
|
||||||
|
} from 'vuex';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
prop: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
value: {
|
||||||
|
type: [String, Number],
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
unit: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
width: {
|
||||||
|
type: [String, Number],
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
slotContent: {
|
||||||
|
type: [String, Boolean],
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
slotValue: {
|
||||||
|
type: [String, Boolean],
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const store = useStore();
|
||||||
|
|
||||||
|
const columnContentRef = ref(null);
|
||||||
|
let resizeObserver = null;
|
||||||
|
|
||||||
|
const columnWidth = computed(() => store.state?.serverListColumnWidths?.[props.prop]);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (columnContentRef.value) {
|
||||||
|
resizeObserver = new ResizeObserver((entries) => {
|
||||||
|
entries.forEach((entry) => {
|
||||||
|
let { width } = entry.contentRect;
|
||||||
|
width = Math.ceil(width);
|
||||||
|
store.dispatch('setServerListColumnWidth', {
|
||||||
|
prop: props.prop,
|
||||||
|
width: width > 40 ? width : 40,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
resizeObserver.observe(columnContentRef.value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
if (resizeObserver) {
|
||||||
|
resizeObserver.disconnect();
|
||||||
|
resizeObserver = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const columnStyle = computed(() => {
|
||||||
|
const style = {};
|
||||||
|
if (props.width) {
|
||||||
|
const width = parseInt(props.width, 10);
|
||||||
|
if (Number.isNaN(width) === false) {
|
||||||
|
style.width = `${width}px`;
|
||||||
|
}
|
||||||
|
} else if (columnWidth.value > 0) {
|
||||||
|
style.width = `${columnWidth.value}px`;
|
||||||
|
}
|
||||||
|
return style;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.list-column {
|
||||||
|
--list-column-label-height: 16px;
|
||||||
|
--list-column-value-height: 24px;
|
||||||
|
position: relative;
|
||||||
|
width: auto;
|
||||||
|
height: calc(var(--list-column-label-height) + var(--list-column-value-height) + 10px);
|
||||||
|
|
||||||
|
.list-column-content {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
width: max-content;
|
||||||
|
height: var(--list-item-height);
|
||||||
|
|
||||||
|
.item-label {
|
||||||
|
padding-top: 6px; // 视觉修正
|
||||||
|
line-height: var(--list-column-label-height);
|
||||||
|
font-size: 12px;
|
||||||
|
color: #bbb;
|
||||||
|
}
|
||||||
|
.item-content {
|
||||||
|
line-height: var(--list-column-value-height);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&--duration {
|
||||||
|
.item-value {
|
||||||
|
color: var(--duration-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&--load {
|
||||||
|
.item-value {
|
||||||
|
color: var(--load-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&--transfer {
|
||||||
|
.item-value {
|
||||||
|
color: var(--transfer-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&--inTransfer {
|
||||||
|
.item-value {
|
||||||
|
color: var(--transfer-in-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&--outTransfer {
|
||||||
|
.item-value {
|
||||||
|
color: var(--transfer-out-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&--speeds {
|
||||||
|
.item-value {
|
||||||
|
color: var(--net-speed-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&--inSpeed {
|
||||||
|
.item-value {
|
||||||
|
color: var(--net-speed-in-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&--outSpeed {
|
||||||
|
.item-value {
|
||||||
|
color: var(--net-speed-out-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&--remaining-time {
|
||||||
|
.value-text {
|
||||||
|
color: #74dbef;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&--billing {
|
||||||
|
.value-text {
|
||||||
|
color: var(--list-item-price-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&--tcp {
|
||||||
|
.item-value {
|
||||||
|
color: var(--conn-tcp-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&--udp {
|
||||||
|
.item-value {
|
||||||
|
color: var(--conn-udp-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&--conns {
|
||||||
|
.item-value {
|
||||||
|
color: var(--conn-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -0,0 +1,88 @@
|
|||||||
|
<template>
|
||||||
|
<server-list-column
|
||||||
|
v-if="extraFields?.remainingTime"
|
||||||
|
prop="remaining-time"
|
||||||
|
label="剩余"
|
||||||
|
:value="billAndPlan?.remainingTime?.value || '-'"
|
||||||
|
/>
|
||||||
|
<server-list-column
|
||||||
|
v-if="extraFields?.billing"
|
||||||
|
prop="billing"
|
||||||
|
label="费用"
|
||||||
|
:value="billAndPlan?.billing?.value || '-'"
|
||||||
|
/>
|
||||||
|
<server-list-column
|
||||||
|
v-if="extraFields?.orderLink"
|
||||||
|
prop="order-link"
|
||||||
|
label="链接"
|
||||||
|
:slot-content="true"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
v-if="showBuyBtn"
|
||||||
|
class="order-link"
|
||||||
|
@click="toBuy"
|
||||||
|
>
|
||||||
|
{{ buyBtnText }}
|
||||||
|
</span>
|
||||||
|
<span v-else>-</span>
|
||||||
|
</server-list-column>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
/**
|
||||||
|
* 套餐信息
|
||||||
|
*/
|
||||||
|
import {
|
||||||
|
inject,
|
||||||
|
computed,
|
||||||
|
} from 'vue';
|
||||||
|
|
||||||
|
import config from '@/config';
|
||||||
|
|
||||||
|
import handleServerBillAndPlan from '@/views/composable/server-bill-and-plan';
|
||||||
|
|
||||||
|
import ServerListColumn from './server-list-column.vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
info: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const filterServerList = inject('filterServerList', {
|
||||||
|
value: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const extraFields = computed(() => filterServerList.value?.fields || {});
|
||||||
|
|
||||||
|
const {
|
||||||
|
billAndPlan,
|
||||||
|
} = handleServerBillAndPlan({
|
||||||
|
props,
|
||||||
|
});
|
||||||
|
|
||||||
|
const buyBtnText = computed(() => {
|
||||||
|
if (props.info?.PublicNote?.customData?.buyBtnText) {
|
||||||
|
return props.info?.PublicNote?.customData?.buyBtnText;
|
||||||
|
}
|
||||||
|
return config.nazhua.buyBtnText || '购买';
|
||||||
|
});
|
||||||
|
const showBuyBtn = computed(() => !!props.info?.PublicNote?.customData?.orderLink);
|
||||||
|
|
||||||
|
function toBuy() {
|
||||||
|
const decodeUrl = decodeURIComponent(props.info?.PublicNote?.customData?.orderLink);
|
||||||
|
window.open(decodeUrl, '_blank');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.order-link {
|
||||||
|
color: var(--list-item-buy-link-color);
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--list-item-buy-link-color-hover);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -0,0 +1,45 @@
|
|||||||
|
<template>
|
||||||
|
<server-list-column
|
||||||
|
v-for="item in serverRealTimeList"
|
||||||
|
:key="item.key"
|
||||||
|
:prop="item.key"
|
||||||
|
:label="item.label"
|
||||||
|
:value="item.show ? item?.value : '-'"
|
||||||
|
:unit="item.show ? item?.unit : ''"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
/**
|
||||||
|
* 服务器数据统计
|
||||||
|
*/
|
||||||
|
import {
|
||||||
|
inject,
|
||||||
|
} from 'vue';
|
||||||
|
import handleServerRealTime from '@/views/composable/server-real-time';
|
||||||
|
|
||||||
|
import ServerListColumn from './server-list-column.vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
info: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
serverRealTimeListTpls: {
|
||||||
|
type: String,
|
||||||
|
default: undefined,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const currentTime = inject('currentTime', {
|
||||||
|
value: Date.now(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
serverRealTimeList,
|
||||||
|
} = handleServerRealTime({
|
||||||
|
props,
|
||||||
|
currentTime,
|
||||||
|
serverRealTimeListTpls: props.serverRealTimeListTpls,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@ -0,0 +1,143 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="server-list-item-status-progress"
|
||||||
|
:class="'server-status--' + type"
|
||||||
|
:title="valPercent"
|
||||||
|
>
|
||||||
|
<span class="progress-label">
|
||||||
|
{{ label }}
|
||||||
|
</span>
|
||||||
|
<div class="progress-bar">
|
||||||
|
<div class="progress-bar-box">
|
||||||
|
<div
|
||||||
|
class="progress-bar-inner"
|
||||||
|
:style="progressStyle"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
class="progress-bar-used"
|
||||||
|
>
|
||||||
|
{{ valText }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
/**
|
||||||
|
* 服务器状态进度调单项
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
computed,
|
||||||
|
} from 'vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
type: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
type: Number,
|
||||||
|
default: 100,
|
||||||
|
},
|
||||||
|
used: {
|
||||||
|
type: [Number, String],
|
||||||
|
default: 1,
|
||||||
|
},
|
||||||
|
colors: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
valText: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
valPercent: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
type: [String, Object],
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const progressStyle = computed(() => {
|
||||||
|
const style = {};
|
||||||
|
style.width = `${Math.min(props.used, 100)}%`;
|
||||||
|
const color = typeof props.colors === 'string' ? props.colors : props.colors?.used;
|
||||||
|
if (color) {
|
||||||
|
if (Array.isArray(color)) {
|
||||||
|
style.background = `linear-gradient(-35deg, ${color.join(',')})`;
|
||||||
|
} else {
|
||||||
|
style.backgroundColor = color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return style;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.server-list-item-status-progress {
|
||||||
|
--progress-label-height: 16px;
|
||||||
|
--progress-bar-height: 24px;
|
||||||
|
--progress-bar-box-height: 14px;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: var(--list-item-height);
|
||||||
|
|
||||||
|
.progress-label {
|
||||||
|
padding-top: 6px; // 视觉修正
|
||||||
|
line-height: var(--progress-label-height);
|
||||||
|
font-size: 12px;
|
||||||
|
color: #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
height: var(--progress-bar-height);
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar-box {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: var(--progress-bar-box-height);
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
border-radius: calc(var(--progress-bar-box-height) / 2);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar-inner {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: #08f;
|
||||||
|
border-radius: calc(var(--progress-bar-box-height) / 2);
|
||||||
|
box-shadow: 2px 0 2px rgba(#000, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar-used {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
line-height: var(--progress-bar-box-height);
|
||||||
|
font-size: 12px;
|
||||||
|
text-align: center;
|
||||||
|
text-shadow: 1px 1px 2px rgba(#000, 0.8), 0 0 1px rgba(#fff, 0.5);
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -0,0 +1,71 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
v-for="item in serverStatusList"
|
||||||
|
:key="item.type"
|
||||||
|
class="list-column-item list-column-item--status"
|
||||||
|
:class="`list-column-item--status-${componentName} list-column-item--status-type-${item.type}`"
|
||||||
|
>
|
||||||
|
<component
|
||||||
|
:is="componentMaps[componentName]"
|
||||||
|
:type="item.type"
|
||||||
|
:used="item.used"
|
||||||
|
:colors="item.colors"
|
||||||
|
:val-text="item.valPercent"
|
||||||
|
:val-percent="`${item.label}使用${item.valText}`"
|
||||||
|
:label="item.label"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
/**
|
||||||
|
* 服务器状态盒子
|
||||||
|
*/
|
||||||
|
|
||||||
|
import config from '@/config';
|
||||||
|
|
||||||
|
import handleServerStatus from '@/views/composable/server-status';
|
||||||
|
import ServerStatusDonut from '@/views/components/server/server-status-donut.vue';
|
||||||
|
import ServerStatusProgress from './server-list-item-status-progress.vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
info: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const componentMaps = {
|
||||||
|
donut: ServerStatusDonut,
|
||||||
|
progress: ServerStatusProgress,
|
||||||
|
};
|
||||||
|
|
||||||
|
const componentName = [
|
||||||
|
'donut',
|
||||||
|
'progress',
|
||||||
|
].includes(config.nazhua.listServerStatusType) ? config.nazhua.listServerStatusType : 'donut';
|
||||||
|
|
||||||
|
const {
|
||||||
|
serverStatusList,
|
||||||
|
} = handleServerStatus({
|
||||||
|
props,
|
||||||
|
statusListTpl: 'cpu,mem,disk',
|
||||||
|
statusListItemContent: false,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.list-column-item {
|
||||||
|
&--status-progress {
|
||||||
|
width: 72px;
|
||||||
|
padding: 0 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--status-donut {
|
||||||
|
--server-status-size: 66px;
|
||||||
|
--server-status-label-scale: 0.8;
|
||||||
|
--server-status-val-text-font-size: 16px;
|
||||||
|
--server-status-label-font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
168
src/views/components/server-list/row/server-list-item.vue
Normal file
168
src/views/components/server-list/row/server-list-item.vue
Normal file
@ -0,0 +1,168 @@
|
|||||||
|
<template>
|
||||||
|
<dot-dot-box
|
||||||
|
border-radius="var(--list-item-border-radius)"
|
||||||
|
padding="var(--list-item-padding)"
|
||||||
|
class="server-list-row-item"
|
||||||
|
:class="{
|
||||||
|
'server-list-row-item--offline': info.online === -1,
|
||||||
|
}"
|
||||||
|
@click="openDetail"
|
||||||
|
>
|
||||||
|
<div class="list-column-item list-column-item--server-flag">
|
||||||
|
<server-flag :info="info" />
|
||||||
|
</div>
|
||||||
|
<div class="list-column-item list-column-item--server-name">
|
||||||
|
<span
|
||||||
|
class="server-name"
|
||||||
|
:title="info.Name"
|
||||||
|
>
|
||||||
|
{{ info.Name }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<server-list-column
|
||||||
|
prop="server-flag"
|
||||||
|
label="地区"
|
||||||
|
:value="info?.Host?.CountryCode?.toUpperCase() || 'UN'"
|
||||||
|
/>
|
||||||
|
<server-list-column
|
||||||
|
prop="server-system"
|
||||||
|
label="系统"
|
||||||
|
:value="platformSystemLabel || '-'"
|
||||||
|
/>
|
||||||
|
<server-list-column
|
||||||
|
prop="cpu-mem"
|
||||||
|
label="配置"
|
||||||
|
:value="cpuAndMemAndDisk || '-'"
|
||||||
|
/>
|
||||||
|
<server-list-item-status
|
||||||
|
v-if="$config.nazhua.hideListItemStatusDonut !== true"
|
||||||
|
:info="info"
|
||||||
|
/>
|
||||||
|
<server-list-item-real-time
|
||||||
|
v-if="$config.nazhua.hideListItemStat !== true"
|
||||||
|
:info="info"
|
||||||
|
server-real-time-list-tpls="load,conns,speeds,transfer,duration"
|
||||||
|
/>
|
||||||
|
<server-list-item-bill
|
||||||
|
v-if="$config.nazhua.hideListItemBill !== true"
|
||||||
|
:info="info"
|
||||||
|
/>
|
||||||
|
</dot-dot-box>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
/**
|
||||||
|
* 单节点
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
computed,
|
||||||
|
} from 'vue';
|
||||||
|
import {
|
||||||
|
useRouter,
|
||||||
|
} from 'vue-router';
|
||||||
|
import * as hostUtils from '@/utils/host';
|
||||||
|
|
||||||
|
import handleServerInfo from '@/views/composable/server-info';
|
||||||
|
import ServerListColumn from './server-list-column.vue';
|
||||||
|
import ServerListItemStatus from './server-list-item-status.vue';
|
||||||
|
import ServerListItemRealTime from './server-list-item-real-time.vue';
|
||||||
|
import ServerListItemBill from './server-list-item-bill.vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
info: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* XCore XGB
|
||||||
|
*/
|
||||||
|
const { cpuAndMemAndDisk } = handleServerInfo({
|
||||||
|
props,
|
||||||
|
});
|
||||||
|
|
||||||
|
const platformSystemLabel = computed(() => hostUtils.getSystemOSLabel(props.info?.Host?.Platform, true));
|
||||||
|
|
||||||
|
function openDetail() {
|
||||||
|
router.push({
|
||||||
|
name: 'ServerDetail',
|
||||||
|
params: {
|
||||||
|
serverId: props.info.ID,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.server-list-row-item {
|
||||||
|
--list-item-height: 64px;
|
||||||
|
--list-item-border-radius: 8px;
|
||||||
|
--list-item-gap: 0;
|
||||||
|
--list-item-padding: 0 20px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
height: var(--list-item-height);
|
||||||
|
gap: var(--list-item-gap);
|
||||||
|
transition: 0.3s;
|
||||||
|
|
||||||
|
&--offline {
|
||||||
|
filter: grayscale(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1280px) {
|
||||||
|
--list-item-padding: 0 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-column-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
&--server-flag {
|
||||||
|
--server-flag-size: 24px;
|
||||||
|
width: calc(var(--server-flag-size) * 1.5);
|
||||||
|
.server-flag {
|
||||||
|
width: calc(var(--server-flag-size) * 1.5);
|
||||||
|
height: var(--server-flag-size);
|
||||||
|
line-height: var(--server-flag-size);
|
||||||
|
font-size: var(--server-flag-size);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1280px) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&--server-name {
|
||||||
|
width: 220px;
|
||||||
|
|
||||||
|
.server-name {
|
||||||
|
height: 32px;
|
||||||
|
line-height: 34px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
width: 100%;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1280px) {
|
||||||
|
width: 180px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
width: 300px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
131
src/views/components/server-list/server-list-warp.vue
Normal file
131
src/views/components/server-list/server-list-warp.vue
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
<template>
|
||||||
|
<transition-group
|
||||||
|
v-if="showTransition"
|
||||||
|
name="list"
|
||||||
|
tag="div"
|
||||||
|
class="server-list-container"
|
||||||
|
:class="{
|
||||||
|
'server-list--row': showListRow,
|
||||||
|
'server-list--card': showListCard,
|
||||||
|
'server-list--status': showListByServerStatus,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</transition-group>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="server-list-container"
|
||||||
|
:class="{
|
||||||
|
'server-list--row': showListRow,
|
||||||
|
'server-list--card': showListCard,
|
||||||
|
'server-list--status': showListByServerStatus,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
/**
|
||||||
|
* 服务器列表
|
||||||
|
*/
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
showTransition: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
showListRow: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
showListCard: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
showListByServerStatus: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.server-list-container.server-list--card {
|
||||||
|
--list-padding: 20px;
|
||||||
|
--list-gap-size: 20px;
|
||||||
|
--list-item-num: 3;
|
||||||
|
--list-item-width: calc(
|
||||||
|
(
|
||||||
|
var(--list-container-width)
|
||||||
|
- (var(--list-padding) * 2)
|
||||||
|
- (
|
||||||
|
var(--list-gap-size)
|
||||||
|
* (var(--list-item-num) - 1)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
/ var(--list-item-num)
|
||||||
|
);
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: var(--list-gap-size);
|
||||||
|
padding: 0 var(--list-padding);
|
||||||
|
width: var(--list-container-width);
|
||||||
|
margin: auto;
|
||||||
|
|
||||||
|
// 针对1440px以下的屏幕
|
||||||
|
@media screen and (max-width: 1440px) {
|
||||||
|
--list-gap-size: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 1024px) {
|
||||||
|
--list-item-num: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 680px) {
|
||||||
|
--list-item-num: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.server-list-container.server-list--row {
|
||||||
|
--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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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-enter-active,
|
||||||
|
.list-leave-active {
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
.list-enter-from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-30px);
|
||||||
|
}
|
||||||
|
.list-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(30px);
|
||||||
|
}
|
||||||
|
.list-leave-active {
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -1,15 +1,32 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="server-option-box">
|
<div
|
||||||
|
class="server-option-box"
|
||||||
|
:class="{
|
||||||
|
'server-option-box--light-background': lightBackground,
|
||||||
|
'server-option-box--mobile-hide': !mobileShow,
|
||||||
|
}"
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
v-for="item in options"
|
v-for="item in options"
|
||||||
:key="item.key"
|
:key="item.key"
|
||||||
class="server-option-item"
|
class="server-option-item"
|
||||||
:class="{
|
:class="{
|
||||||
|
'has-icon': item.icon,
|
||||||
active: activeValue === item.value,
|
active: activeValue === item.value,
|
||||||
}"
|
}"
|
||||||
|
:title="item?.title || false"
|
||||||
@click="toggleModelValue(item)"
|
@click="toggleModelValue(item)"
|
||||||
>
|
>
|
||||||
<span class="option-label">{{ item.label }}</span>
|
<i
|
||||||
|
v-if="item.icon"
|
||||||
|
class="option-icon"
|
||||||
|
:class="item.icon"
|
||||||
|
:title="item.label"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
v-else
|
||||||
|
class="option-label"
|
||||||
|
>{{ item.label }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -21,6 +38,7 @@
|
|||||||
import {
|
import {
|
||||||
computed,
|
computed,
|
||||||
} from 'vue';
|
} from 'vue';
|
||||||
|
import config from '@/config';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
modelValue: {
|
modelValue: {
|
||||||
@ -31,7 +49,11 @@ const props = defineProps({
|
|||||||
type: Array,
|
type: Array,
|
||||||
default: () => [],
|
default: () => [],
|
||||||
},
|
},
|
||||||
accpetEmpty: {
|
acceptEmpty: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
mobileShow: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
@ -41,6 +63,8 @@ const emits = defineEmits([
|
|||||||
'update:modelValue',
|
'update:modelValue',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const lightBackground = computed(() => config.nazhua.lightBackground);
|
||||||
|
|
||||||
const activeValue = computed({
|
const activeValue = computed({
|
||||||
get: () => props.modelValue,
|
get: () => props.modelValue,
|
||||||
set: (val) => {
|
set: (val) => {
|
||||||
@ -50,7 +74,7 @@ const activeValue = computed({
|
|||||||
|
|
||||||
function toggleModelValue(item) {
|
function toggleModelValue(item) {
|
||||||
if (activeValue.value === item.value) {
|
if (activeValue.value === item.value) {
|
||||||
if (props.accpetEmpty) {
|
if (props.acceptEmpty) {
|
||||||
activeValue.value = '';
|
activeValue.value = '';
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -66,6 +90,12 @@ function toggleModelValue(item) {
|
|||||||
padding: 0 var(--list-padding);
|
padding: 0 var(--list-padding);
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
|
||||||
|
@media screen and (max-width: 768px) {
|
||||||
|
&--mobile-hide {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.server-option-item {
|
.server-option-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -74,18 +104,62 @@ function toggleModelValue(item) {
|
|||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
background: rgba(#000, 0.3);
|
background: rgba(#000, 0.3);
|
||||||
|
transition: all 0.3s linear;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
|
&.has-icon {
|
||||||
|
padding: 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 768px) {
|
@media screen and (max-width: 768px) {
|
||||||
|
height: 30px;
|
||||||
|
padding: 0 10px;
|
||||||
|
border-radius: 3px;
|
||||||
background-color: rgba(#000, 0.8);
|
background-color: rgba(#000, 0.8);
|
||||||
cursor: default;
|
cursor: default;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.option-icon {
|
||||||
|
line-height: 1;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
.option-label {
|
.option-label {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
|
font-weight: bold;
|
||||||
|
transition: all 0.3s linear;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 768px) {
|
||||||
|
&:hover {
|
||||||
|
.option-label {
|
||||||
|
color: var(--option-high-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
background: rgba(#ff7500, 0.75);
|
background: var(--option-high-color-active);
|
||||||
|
|
||||||
|
.option-label {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 768px) {
|
||||||
|
&--light-background {
|
||||||
|
.server-option-item {
|
||||||
|
background: rgba(#000, 0.5);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(#000, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background: var(--option-high-color-active);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
341
src/views/components/server-list/server-sort-box.vue
Normal file
341
src/views/components/server-list/server-sort-box.vue
Normal file
@ -0,0 +1,341 @@
|
|||||||
|
<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>
|
||||||
145
src/views/components/server-list/server-sort-dropdown-menu.vue
Normal file
145
src/views/components/server-list/server-sort-dropdown-menu.vue
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
<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>
|
||||||
142
src/views/components/server-list/server-status/main.vue
Normal file
142
src/views/components/server-list/server-status/main.vue
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
<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>
|
||||||
@ -0,0 +1,68 @@
|
|||||||
|
<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>
|
||||||
@ -0,0 +1,35 @@
|
|||||||
|
<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>
|
||||||
@ -0,0 +1,74 @@
|
|||||||
|
<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>
|
||||||
@ -0,0 +1,47 @@
|
|||||||
|
<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>
|
||||||
@ -0,0 +1,39 @@
|
|||||||
|
<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>
|
||||||
@ -0,0 +1,74 @@
|
|||||||
|
<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>
|
||||||
446
src/views/components/server-list/server-status/server-status.js
Normal file
446
src/views/components/server-list/server-status/server-status.js
Normal file
@ -0,0 +1,446 @@
|
|||||||
|
/**
|
||||||
|
* 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,
|
||||||
|
};
|
||||||
|
};
|
||||||
209
src/views/components/server-list/server-status/table/td.vue
Normal file
209
src/views/components/server-list/server-status/table/td.vue
Normal file
@ -0,0 +1,209 @@
|
|||||||
|
<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>
|
||||||
72
src/views/components/server-list/server-status/table/th.vue
Normal file
72
src/views/components/server-list/server-status/table/th.vue
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
<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>
|
||||||
@ -7,10 +7,42 @@
|
|||||||
:class="`server-real-time--${item.key}`"
|
:class="`server-real-time--${item.key}`"
|
||||||
>
|
>
|
||||||
<div class="item-content">
|
<div class="item-content">
|
||||||
<span class="item-value">{{ item?.value || '-' }}</span>
|
<div
|
||||||
<span class="item-unit item-text">{{ item?.value ? item?.unit : '' }}</span>
|
v-if="item.show && item.values"
|
||||||
|
class="item-content-sub-group"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
v-for="subItem in item.values"
|
||||||
|
:key="`${item.key}_${subItem.key}`"
|
||||||
|
class="item-content-sub-item"
|
||||||
|
:class="`item-content-sub-item--${item.key}-${subItem.key}`"
|
||||||
|
>
|
||||||
|
<span class="item-content-sub-label">
|
||||||
|
{{ subItem.label }}
|
||||||
|
</span>
|
||||||
|
<span class="item-content-sub-content">
|
||||||
|
<span class="item-value">{{ subItem.show ? subItem?.value : '-' }}</span>
|
||||||
|
<span
|
||||||
|
v-if="subItem.show"
|
||||||
|
class="item-unit item-text"
|
||||||
|
>{{ subItem?.unit }}</span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<span class="item-label">{{ item.label }}</span>
|
<template v-else>
|
||||||
|
<span class="item-value">{{ item.show ? item?.value : '-' }}</span>
|
||||||
|
<span
|
||||||
|
v-if="item.show"
|
||||||
|
class="item-unit item-text"
|
||||||
|
>{{ item?.unit }}</span>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
v-if="!item.values"
|
||||||
|
class="item-label"
|
||||||
|
>
|
||||||
|
{{ item.label }}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -29,6 +61,10 @@ const props = defineProps({
|
|||||||
type: Object,
|
type: Object,
|
||||||
default: () => ({}),
|
default: () => ({}),
|
||||||
},
|
},
|
||||||
|
serverRealTimeListTpls: {
|
||||||
|
type: String,
|
||||||
|
default: undefined,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const currentTime = inject('currentTime', {
|
const currentTime = inject('currentTime', {
|
||||||
@ -40,6 +76,7 @@ const {
|
|||||||
} = handleServerRealTime({
|
} = handleServerRealTime({
|
||||||
props,
|
props,
|
||||||
currentTime,
|
currentTime,
|
||||||
|
serverRealTimeListTpls: props.serverRealTimeListTpls,
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -78,6 +115,86 @@ const {
|
|||||||
font-size: var(--real-time-label-font-size, 14px);
|
font-size: var(--real-time-label-font-size, 14px);
|
||||||
color: #ddd;
|
color: #ddd;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.item-content-sub-group {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
.item-content-sub-item {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
--real-time-label-line-height: calc(var(--real-time-label-font-size, 14px) * 1.8);
|
||||||
|
|
||||||
|
.item-content-sub-label {
|
||||||
|
height: var(--real-time-label-line-height);
|
||||||
|
line-height: var(--real-time-label-line-height);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-content-sub-content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-value,
|
||||||
|
.item-text,
|
||||||
|
.item-label {
|
||||||
|
height: var(--real-time-label-line-height);
|
||||||
|
line-height: var(--real-time-label-line-height);
|
||||||
|
font-size: var(--real-time-label-font-size, 14px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-content-sub-item--L-A-P-load {
|
||||||
|
.item-value {
|
||||||
|
color: var(--load-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.item-content-sub-item--L-A-P-process {
|
||||||
|
.item-value {
|
||||||
|
color: var(--process-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-content-sub-item--D-A-T-duration {
|
||||||
|
.item-value {
|
||||||
|
color: var(--duration-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.item-content-sub-item--D-A-T-transfer {
|
||||||
|
.item-value {
|
||||||
|
color: var(--transfer-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-content-sub-item--speeds-in {
|
||||||
|
.item-value {
|
||||||
|
color: var(--net-speed-in-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.item-content-sub-item--speeds-out {
|
||||||
|
.item-value {
|
||||||
|
color: var(--net-speed-out-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-content-sub-item--conn-tcp {
|
||||||
|
.item-value {
|
||||||
|
color: var(--conn-tcp-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.item-content-sub-item--conn-udp {
|
||||||
|
.item-value {
|
||||||
|
color: var(--conn-udp-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.server-real-time--duration {
|
.server-real-time--duration {
|
||||||
@ -90,7 +207,8 @@ const {
|
|||||||
color: var(--transfer-color);
|
color: var(--transfer-color);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.server-real-time--inSpeed {
|
.server-real-time--inSpeed,
|
||||||
|
.server-real-time--speed {
|
||||||
.item-value {
|
.item-value {
|
||||||
color: var(--net-speed-in-color);
|
color: var(--net-speed-in-color);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,7 +12,7 @@
|
|||||||
<template #default>
|
<template #default>
|
||||||
<div
|
<div
|
||||||
class="chart-donut-label"
|
class="chart-donut-label"
|
||||||
:title="`${(used).toFixed(1) * 1}%`"
|
:title="valPercent ? valPercent : `${(used).toFixed(1) * 1}%`"
|
||||||
>
|
>
|
||||||
<div class="server-status-val-text">
|
<div class="server-status-val-text">
|
||||||
<span>{{ valText }}</span>
|
<span>{{ valText }}</span>
|
||||||
@ -73,6 +73,10 @@ defineProps({
|
|||||||
type: String,
|
type: String,
|
||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
|
valPercent: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
label: {
|
label: {
|
||||||
type: String,
|
type: String,
|
||||||
default: '',
|
default: '',
|
||||||
@ -103,24 +107,25 @@ defineProps({
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
transform: scale(var(--server-status-label-scale, 1));
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.server-status-val-text {
|
.server-status-val-text {
|
||||||
line-height: 1.2em;
|
line-height: 1.2em;
|
||||||
font-size: var(--server-status-val-text-font-size, 14px);
|
font-size: var(--server-status-val-text-font-size, 14px);
|
||||||
color: #a1eafb;
|
color: var(--server-status-value-color);
|
||||||
}
|
}
|
||||||
.server-status-label {
|
.server-status-label {
|
||||||
line-height: 1.1em;
|
line-height: 1.1em;
|
||||||
font-size: var(--server-status-label-font-size, 12px);
|
font-size: var(--server-status-label-font-size, 12px);
|
||||||
color: #ddd;
|
color: var(--server-status-label-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.server-status-content {
|
.server-status-content {
|
||||||
line-height: 1.2em;
|
line-height: 1.2em;
|
||||||
font-size: var(--server-status-content-font-size, 14px);
|
font-size: var(--server-status-content-font-size, 14px);
|
||||||
color: #eee;
|
color: var(--server-status-content-color);
|
||||||
|
|
||||||
.default-mobile {
|
.default-mobile {
|
||||||
display: none;
|
display: none;
|
||||||
|
|||||||
@ -12,7 +12,10 @@
|
|||||||
class="progress-bar-label"
|
class="progress-bar-label"
|
||||||
:title="label + '使用' + used + '%'"
|
:title="label + '使用' + used + '%'"
|
||||||
>
|
>
|
||||||
<span class="server-status-label">
|
<span
|
||||||
|
v-if="label"
|
||||||
|
class="server-status-label"
|
||||||
|
>
|
||||||
{{ label }}:
|
{{ label }}:
|
||||||
</span>
|
</span>
|
||||||
<span class="server-status-val-text">
|
<span class="server-status-val-text">
|
||||||
@ -75,8 +78,12 @@ const progressStyle = computed(() => {
|
|||||||
style.width = `${Math.min(props.used, 100)}%`;
|
style.width = `${Math.min(props.used, 100)}%`;
|
||||||
const color = typeof props.colors === 'string' ? props.colors : props.colors?.used;
|
const color = typeof props.colors === 'string' ? props.colors : props.colors?.used;
|
||||||
if (color) {
|
if (color) {
|
||||||
|
if (Array.isArray(color)) {
|
||||||
|
style.background = `linear-gradient(-35deg, ${color.join(',')})`;
|
||||||
|
} else {
|
||||||
style.backgroundColor = color;
|
style.backgroundColor = color;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return style;
|
return style;
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
@ -92,20 +99,20 @@ const progressStyle = computed(() => {
|
|||||||
|
|
||||||
@media screen and (max-width: 480px) {
|
@media screen and (max-width: 480px) {
|
||||||
flex: none;
|
flex: none;
|
||||||
width: calc(50% - 5px);
|
width: var(--progress-bar-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;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: var(--progress-bar-height);
|
height: var(--progress-bar-height);
|
||||||
background: rgba(255, 255, 255, 0.2);
|
background: rgba(255, 255, 255, 0.2);
|
||||||
border-radius: var(--progress-bar-height);
|
border-radius: calc(var(--progress-bar-height) / 2);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -115,7 +122,7 @@ const progressStyle = computed(() => {
|
|||||||
left: 0;
|
left: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
background-color: #08f;
|
background-color: #08f;
|
||||||
border-radius: var(--progress-bar-height);
|
border-radius: calc(var(--progress-bar-height) / 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.progress-bar-label {
|
.progress-bar-label {
|
||||||
|
|||||||
@ -25,7 +25,8 @@ export default (params) => {
|
|||||||
billingDataMod,
|
billingDataMod,
|
||||||
planDataMod,
|
planDataMod,
|
||||||
} = props.info.PublicNote;
|
} = props.info.PublicNote;
|
||||||
let months;
|
// 默认1个月
|
||||||
|
let months = 1;
|
||||||
// 套餐资费
|
// 套餐资费
|
||||||
let cycleLabel;
|
let cycleLabel;
|
||||||
if (validate.isSet(billingDataMod?.cycle)) {
|
if (validate.isSet(billingDataMod?.cycle)) {
|
||||||
@ -60,25 +61,29 @@ export default (params) => {
|
|||||||
months = 6;
|
months = 6;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
cycleLabel = billingDataMod.cycle;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (validate.isSet(billingDataMod?.amount)) {
|
if (validate.isSet(billingDataMod?.amount)) {
|
||||||
|
let isFree = false;
|
||||||
let amountValue = billingDataMod.amount;
|
let amountValue = billingDataMod.amount;
|
||||||
let label;
|
let label;
|
||||||
if (billingDataMod.amount.toString() === '-1') {
|
if (billingDataMod.amount.toString() === '-1') {
|
||||||
amountValue = '按量';
|
amountValue = '按量';
|
||||||
label = `每${cycleLabel}`;
|
label = cycleLabel ? `每${cycleLabel}` : '';
|
||||||
} else if (billingDataMod.amount.toString() === '0') {
|
} else if (billingDataMod.amount.toString() === '0') {
|
||||||
amountValue = config.nazhua.freeAmount || '免费';
|
amountValue = config.nazhua.freeAmount || '免费';
|
||||||
|
isFree = true;
|
||||||
} else {
|
} else {
|
||||||
label = `${cycleLabel}付`;
|
label = cycleLabel ? `${cycleLabel}付` : '';
|
||||||
}
|
}
|
||||||
obj.billing = {
|
obj.billing = {
|
||||||
label,
|
label,
|
||||||
value: amountValue,
|
value: amountValue,
|
||||||
cycleLabel,
|
cycleLabel,
|
||||||
months,
|
months,
|
||||||
|
isFree,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
// 剩余时间
|
// 剩余时间
|
||||||
@ -91,8 +96,8 @@ export default (params) => {
|
|||||||
const endTime = dayjs(endDate).valueOf();
|
const endTime = dayjs(endDate).valueOf();
|
||||||
if (endDate.indexOf('0000-00-00') === 0) {
|
if (endDate.indexOf('0000-00-00') === 0) {
|
||||||
obj.remainingTime = {
|
obj.remainingTime = {
|
||||||
label: '有效期',
|
label: '剩余',
|
||||||
value: config.nazhua.infinityCycle || '无限',
|
value: config.nazhua.infinityCycle || '长期有效',
|
||||||
type: 'infinity',
|
type: 'infinity',
|
||||||
};
|
};
|
||||||
} else if (autoRenewal === '1') {
|
} else if (autoRenewal === '1') {
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user