🌐 添加国际化支持,支持中英文切换

This commit is contained in:
hi2hi 2025-02-26 07:04:39 +00:00
parent 842cc7d2f8
commit 9101703f55
29 changed files with 5409 additions and 3228 deletions

4604
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -20,6 +20,7 @@
"uniqolor": "^1.1.1",
"vue": "^3.5.12",
"vue-echarts": "^7.0.3",
"vue-i18n": "^11.1.1",
"vue-router": "^4.4.5",
"vuex": "^4.1.0"
},

View File

@ -1,4 +1,5 @@
window.$$nazhuaConfig = {
// locale: 'en', // 国际化支持 en | zh-CN
// title: '哪吒监控', // 网站标题
// footerSlogan: '不要年付!不要年付!不要年付!<span style="color: #f00;">欢迎访问Nazhua探针</span>',
// freeAmount: '白嫖', // 免费服务的费用名称

323
readme-en.md Normal file
View File

@ -0,0 +1,323 @@
# Nazhua
**[This is an AI-translated version of the original Chinese document. There might be some inaccuracies in translation.]**
**Before using, please make sure to read the content of this Readme, it will be helpful to you**
A frontend theme built based on Nezha Monitoring (nezha.wiki) v0 version, currently compatible with v1 version which has the same data structure as v0.
~~The theme is a bit **heavy** because it includes a `SarasaTermSC-SemiBold` font with Chinese characters.~~
~~Depending on different scenarios, you can choose whether to package and include or load this font.~~
Considering that most users in China with direct connections cannot access jsdelivr, the default is to use the loli.net reference version of cdnjs.
At the same time, the SarasaTermSC font is disabled by default. If you need to use it, please use the Docker image full package.
## Sponsors
> Sorted alphabetically by service provider, no particular order.
<table>
<tr>
<td align="center">
<a href="https://www.vmiss.com" target="_blank" title="VMISS, a Canadian enterprise, creating globally optimized quality routes. Providing cloud servers in Hong Kong, Japan, Korea, USA, and UK">
<img src="./.github/images/vmiss-logo.jpg" width="200px;" alt="VMISS"/>
</a>
<br />
<span style="font-weight: bold;">VMISS</span>
</td>
<td align="center">
<a href="https://yxvm.com" target="_blank" title="YXVM, providing cloud servers and physical servers in Hong Kong, Singapore, and Japan">
<img src="./.github/images/yxvm-logo.jpg" width="200px;" alt="YXVM"/>
</a>
<br />
<span style="font-weight: bold;">YXVM</span>
</td>
</tr>
</table>
## Disclaimer - Must Read Before Use
1. This theme is built based on Nezha Monitoring v0 version, ~~not sure if it's perfectly compatible with v1 version~~. *v0.4.3 version has been adapted*
2. This theme is a pure frontend project, which needs to solve cross-domain issues, usually requiring nginx or caddy reverse proxy to solve cross-domain request problems.
3. I will not provide any technical support. If you have questions, you can submit an issue, but I don't guarantee I'll answer. Asking GPT might be faster.
## Feature Updates
### v0.5.3 Update
New: Support for setting purchase button text and icon individually for servers, requires adding `buyBtnText` and `buyBtnIcon` fields in the customData of public notes.
> Usage: `buyBtnText` is used to set the purchase button text, `buyBtnIcon` is used to set the purchase button icon, supporting Remixicon icon names, e.g.: `ri-gift-2-line`.
> Example: Click to copy the icon name, then fill it in the `buyBtnIcon` field, adding the `ri-` prefix.
> ![remixicon usage method](./.github/images/remixicon-select.jpg)
> Online icon website: [www.remixicon.com](https://www.remixicon.com/) Currently supporting version 4.6.0
## Differences Between V0/V1 Usage
### Docker version of nazhua
For V1, you must specify the `nezhaVersion` as `v1` in `config.js`, **case sensitive*
The default data is based on V0
### Release version of nazhua
For V1, download the latest version [Releases](https://github.com/hi2shark/nazhua/releases) of `dist.zip`;
For V0, download the latest version [Releases](https://github.com/hi2shark/nazhua/releases) of `v0-dist.zip`;
`v{version}-all.zip` is the full package including fonts.
`v{version}-cdn-{CDN provider}.zip` is the version using CDN references for public resources.
## About the Dot Matrix Map
The dot matrix map is a distorted map where the map borders and city positions are not real latitude and longitude coordinates, so cities cannot be located through latitude and longitude.
You need to pick coordinates on the dot matrix map in the [Nazhua Configuration Generator](https://hi2shark.github.io/nazhua-generator/), and then configure `customCodeMap` in `config.js` to customize map point information.
How to specify a node's geographic location?
In the Nezha monitoring backend, add a `customData` object to the node's public notes, and specify the `location` code;
For information about which built-in geographic location codes are available, check the [Nazhua Configuration Generator](https://hi2shark.github.io/nazhua-generator/).
Example
```json
{
"customData": {
"location": "HKG"
}
}
```
For several common country locations I've encountered, default mapping positions have been added, which will automatically display on the map.
Tips: Mainland China defaults to the capital: Beijing (this mapping was added after 0.4.6)
Tips: The United States defaults to the most commonly purchased location: Los Angeles
## About Node Slogan and Purchase Links
At the same time, you can also add `slogan` and `orderLink` strings to this `customData`, used to display the node's slogan and purchase link respectively.
```json
{
"customData": {
"location": "HKG",
"slogan": "This is a Hong Kong node",
"orderLink": "https://buy.example.com"
}
}
```
Tips:
Due to the special way configuration data is obtained, the symbol `&` cannot be parsed normally. It is recommended to encode it at [https://www.bejson.com/enc/urlencode/](https://www.bejson.com/enc/urlencode/), and add the encodeURIComponent encoded content to orderLink.
Of course, you can also execute `encodeURIComponent('link content')` in your browser's console to get the encoded content.
## Support for Public Notes
In the ServerStatus theme iteration of Nezha, nap0o added a public notes feature that allows adding additional display information to nodes
For specific field definitions, refer to [https://github.com/nezhahq/nezha/pull/425](https://github.com/nezhahq/nezha/pull/425)
Nazhua supports about 90% of this, with the following fields being processed:
```json
{
"billingDataMod": {
"startDate": "2024-10-01T00:00:00+08:00",
"endDate": "2024-11-01T00:00:00+08:00",
"autoRenewal": "1",
"cycle": "Month",
"amount": "$3.99"
},
"planDataMod": {
"bandwidth": "30Mbps",
"trafficVol": "1TB/Month",
"trafficType": "1",
"IPv4": "1",
"IPv6": "1",
"networkRoute": "CN2,GIA",
"extra": "Heirloom,AS9929"
}
}
```
~~IPv4 and IPv6 have not yet been included in processing, may be supported in the future.~~
- Both display label: Dual-stack IP;
- IPv4 only display label: IPv4 only;
- IPv6 only display label: IPv6 only;
## Data Sources
1-0. Public full configuration, including "Public Notes" (PublicNote), comes from the server node list configuration information exposed on the probe homepage. This is obtained through regular expression matching of the node list. In the theme project, the default is to direct `/nezha/` to this location.
2-0. Real-time data, v0 comes from the public ws service interface, `/ws`.
2-1. Real-time data/full data, v1 comes from the public ws service interface, `/api/v1/ws/server`.
3-0. Monitoring data, v0 comes from the public api interface, `/api/v1/monitor/${id}`.
3-1. Monitoring data, v1 comes from the public api interface, `/api/v1/service/${id}`.
4-0. Group data, v0 comes from matching the `Tag` field in the server node list.
4-1. Group data, v1 comes from the public api interface, `/api/v1/server-group`.
## Deployment
The Nazhua theme is a pure frontend project that can be deployed on a pure static server;
v0 needs to solve cross-domain access for monitoring data `/api/v1/monitor/${id}`, WS service `/ws`, and homepage `/`.
v1 needs to solve cross-domain access for data interfaces `/api/xxx`, WS service `/api/v1/ws/server`.
Typically, you need nginx or caddy to reverse proxy requests to solve cross-domain issues.
### Docker Compose + Cloudflare Tunnels Deployment
**Please pay attention to the notes in the comments**
```yaml
services:
nazhua:
image: ghcr.io/hi2shark/nazhua:latest
container_name: nazhua
ports:
- 80:80
# volumes:
# - ./favicon.ico:/home/wwwroot/html/favicon.ico:ro # Custom favicon icon
# - ./config.js:/home/wwwroot/html/config.js:ro # Custom configuration file
# - ./style.css:/home/wwwroot/html/style.css:ro # Custom style file
environment:
- DOMAIN=_ # Domain to listen on, default is _ (listen to all)
- NEZHA=http://nezha-dashboard.example.com/ # Nezha homepage address that can be reverse proxied
restart: unless-stopped
```
It is recommended to deploy the service through docker-compose, and then provide the service externally through cloudflare tunnels, so you don't have to configure https certificates yourself.
**Again, for Nezha V1, you must set nezhaVersion to v1 in config.js**
**If you don't want to load the complete built-in library, you can use the cdn reference image**
For example: replace `ghcr.io/hi2shark/nazhua:latest` with `ghcr.io/hi2shark/nazhua:cdn`
>If you want to hide the original panel and only expose nazhua, you can use Zero Trust Tunnels;
>Three containers: Tunnels, nezha-dashboard, nazhua
>Nazhua accesses nezha-dashboard using the docker internal address, then Tunnels binds nazhua to the publicly accessible domain
>Tunnels binds nezha-dashboard to a private domain, requiring email/IP matching for access
### Nginx Configuration Example
```nginx
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
listen 80;
server_name nazhua.example.com;
client_max_body_size 1024m;
# Nezha V0 WebSocket service
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;
}
# Nezha V1 WebSocket service
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;
}
}
```
## Custom Configuration
You can customize the configuration by modifying the `config.js` file in the root directory
For example: (*The reference content in the documentation may not be the latest, please refer to public/config.js or [Nazhua Configuration Generator](https://hi2shark.github.io/nazhua-generator/)*)
```javascript
window.$$nazhuaConfig = {
locale: 'en', // Internationalization support en | zh-CN
title: 'Nezha Monitoring', // Website title
footerSlogan: 'Don\'t pay annually! Don\'t pay annually! Don\'t pay annually! <span style="color: #f00;">Welcome to Nazhua Probe</span>', // Footer slogan
freeAmount: 'Free', // Name for free service fee
infinityCycle: 'Long-term valid', // Name for infinite cycle
buyBtnText: 'Purchase', // Purchase button text
buyBtnIcon: '', // Purchase button icon, from remixicon
customBackgroundImage: '', // Custom background image address
lightBackground: true, // Enable light background image, will forcibly disable dot background
showFireworks: true, // Whether to display fireworks, recommended to enable light background
showLantern: true, // Whether to display lanterns
enableInnerSearch: true, // Enable internal search
listServerItemTypeToggle: true, // Server list item type toggle
listServerItemType: 'row', // Server list item type card/row (row list mode automatically switches to card on mobile)
listServerStatusType: 'progress', // Server status type--list
listServerRealTimeShowLoad: true, // List shows server real-time load
detailServerStatusType: 'progress', // Server status type--detail page
simpleColorMode: true, // Server status pure color display
serverStatusLinear: true, // Server status gradient linear display - mutually exclusive with pureColorMode
disableSarasaTermSC: true, // Disable Sarasa Term SC font
hideWorldMap: false, // Hide map
hideHomeWorldMap: false, // Hide home page map
hideDetailWorldMap: false, // Hide detail map
hideNavbarServerCount: false, // Hide server count
hideNavbarServerStat: false, // Hide server statistics
hideListItemStatusDonut: false, // Hide list item pie chart
hideListItemStat: false, // Hide list item statistics
hideListItemBill: false, // Hide list item billing information
hideFilter: false, // Hide filter
hideTag: false, // Hide tag
hideDotBG: true, // Hide dot background in the frame
monitorRefreshTime: 10, // Monitor refresh time interval in seconds, 0 means no refresh, minimum effective value is 10s to ensure not requesting the source site too frequently
filterGPUKeywords: ['Virtual Display'], // If GPU name contains these keywords, filter it out
customCodeMap: {}, // Custom map point information
nezhaVersion: 'v1', // Nezha version
apiMonitorPath: '/api/v1/monitor/{id}',
wsPath: '/ws',
nezhaPath: '/nezha/',
nezhaV0ConfigType: 'servers', // Nezha v0 data reading type
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 version console address
v1HideNezhaDashboardBtn: true, // v1 version navigation bar console entry/login button, effective when nezhaVersion is v1
routeMode: 'h5', // Route mode
customFavicon: '', // Custom favicon, fill in the complete URL address
};
```
You can quickly generate a config.js configuration file through the [Nazhua Configuration Generator](https://hi2shark.github.io/nazhua-generator/)
Customize styles by modifying the `style.css` file in the root directory
For example:
```css
:root {
/* Modify colors */
/* Color of marker points on the map */
--world-map-point-color: #fff;
/* Color of price displayed in list items */
--list-item-price-color: #ff6;
/* Main color of purchase links */
--list-item-buy-link-color: #f00;
}
```
Custom background image example:
```css
:root {
/* If the image is too bright, the foreground color (also background color) in front of the image needs to be darker */
--layout-main-bg-color: rgba(0, 0, 0, 0.75);
}
/* Custom background image */
.layout-group .layout-bg {
/* Add important to force background image replacement, this replacement design is not very elegant, will be improved later */
background: url(./bg.jpg) no-repeat 50% 50% !important;
background-size: cover;
}
```
`./bg.jpg` is the image address, which can be replaced with an external link image; you can also put the background image in the project, usually through docker volumes mapping, depending on your actual situation.
## Secondary Development Tips
`.env.development.local` configuration variables
```bash
#### Sarasa Term SC font configuration
# VITE_DISABLE_SARASA_TERM_SC=1
# VITE_SARASA_TERM_SC_USE_CDN=1
#### CDN configuration for reference libraries
# VITE_USE_CDN=1
# VITE_CDN_LIB_TYPE=jsdelivr # jsdelivr | cdnjs | loli
#### Nezha default version control
# VITE_NEZHA_VERSION=v1 # v0 | v0
#### Local development settings
# PROXY_WS_HOST= # When developing locally, the address that can proxy WS services, when enabled, automatically forwards to {PROXY_WS_HOST}/proxy?wsPath={WS_HOST}
# API_HOST= # The API service address proxied during local development
# WS_HOST= # The WS service address proxied during local development
##### For v0 version only
# NEZHA_HOST= # The Nezha homepage address proxied during local development
```

View File

@ -217,21 +217,25 @@ server {
例如:(*参考内容在文档上不一定是最新具体参考public/config.js或者[Nazhua配置生成器](https://hi2shark.github.io/nazhua-generator/)*)
```javascript
window.$$nazhuaConfig = {
locale: 'en', // 国际化支持 en | zh-CN
title: '哪吒监控', // 网站标题
footerSlogan: '不要年付!不要年付!不要年付!<span style="color: #f00;">欢迎访问Nazhua探针</span>',
footerSlogan: '不要年付!不要年付!不要年付!<span style="color: #f00;">欢迎访问Nazhua探针</span>', // 底部标语
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', // 服务器状态类型--详情页
serverStatusLinear: true, // 服务器状态渐变线性显示
simpleColorMode: true, // 服务器状态纯色显示
serverStatusLinear: true, // 服务器状态渐变线性显示 - 与pureColorMode互斥
disableSarasaTermSC: true, // 禁用Sarasa Term SC字体
hideWorldMap: false, // 隐藏地图
hideHomeWorldMap: false, // 隐藏首页地图
@ -260,6 +264,7 @@ window.$$nazhuaConfig = {
v1DashboardUrl: '/dashboard', // v1版本控制台地址
v1HideNezhaDashboardBtn: true, // v1版本导航栏控制台入口/登录按钮 在nezhaVersion为v1时有效
routeMode: 'h5', // 路由模式
customFavicon: '', // 自定义favicon, 填写完整的url地址
};
```
可以通过[Nazhua配置生成器](https://hi2shark.github.io/nazhua-generator/)快速生成config.js配置文件

129
src/config/i18n/en.js Normal file
View File

@ -0,0 +1,129 @@
/**
* Internationalization configuration
* English
* (AI translated content)
*/
export default {
// Status
online: 'Online',
offline: 'Offline',
expired: 'Expired',
// General
total: 'Total',
used: 'Used',
remainingTime: 'Remaining',
// Server Related
server: 'Server',
servers: '{count} servers',
serverTotal: 'Total {count} servers',
serverDetail: 'Server Detail',
mapPointServerLabel: '{name}, {count} servers',
// Login and Management
login: 'Login',
dashboard: 'Dashboard',
toDashboard: 'Access Dashboard',
loginDashboard: 'Login to Dashboard',
card: 'Card',
list: 'List',
// Nezha Monitoring
nezhawiki: 'Nezha Monitor',
nezhawikiVersion: 'Current Nezha Monitor',
searchPlaceholder: 'Search server name, tag, OS, country code',
// Network Related
netSpeed: 'Speed',
bandwidth: 'BW',
inTransfer: 'In',
outTransfer: 'Out',
inSpeed: 'In',
outSpeed: 'Out',
speeds: 'Spd',
traffic: 'Traf',
trafficDouble: 'Bidir',
trafficSingleOut: 'Out Only',
trafficSingleMax: 'Max Dir',
trafficMaxOut: 'Max Out',
trafficMaxIn: 'Max In',
ipv4: 'v4',
ipv6: 'v6',
ipv4ipv6: 'Dual',
// Hardware Resources
cpu: 'CPU',
gpu: 'GPU',
mem: 'MEM',
mems: 'RAM {memComputed}',
rams: 'RAM {ramComputed}',
swap: 'SWAP',
swaps: 'SWAP {swapComputed}',
swapMem: 'SWAP {swapComputed}',
disk: 'DISK',
disks: 'DISK {diskComputed}',
diskCapacity: 'DISK {diskComputed}',
// Temperature Related
temperature: 'Temperature',
acpitz: 'Motherboard',
acpitzMean: 'MB Average',
coretempPackage: 'CPU Temp',
coretempCore: 'Core Average',
coretempMaxCore: 'Hottest Core',
// System Status
systemOS: 'Sys',
occupancy: 'Usge',
load: 'Load',
conn: 'Conn',
boot: 'Boot',
active: 'Act',
processCount: 'Processes',
tcp: 'TCP',
udp: 'UDP',
// Tags and Plans
tag: 'Tag',
billPlan: 'Plan',
// Network Monitoring
networkMonitor: 'Network Monitor',
networkMonitorRefresh: 'Auto Refresh',
refreshLabel: 'Refresh',
networkMonitorPeakShaving: 'Peak Cut',
networkMonitorPeakShavingTips: 'Filter extreme values',
networkMonitorLast: 'Last',
avgDelay: 'Avg Lat',
successRate: 'Success %',
// Billing and Ordering
billing: 'Billing',
orderLink: 'Link',
buyBtnText: 'Purchase',
// Time Units
day: 'D',
hour: 'H',
hours: '{count}H',
minute: 'M',
minutes: '{count}M',
// Billing Cycles
monthly: 'Mo',
yearly: 'Yr',
quarterly: 'Qt',
halfYearly: 'Semi',
free: 'Free',
payg: 'PAYG',
cycleLabel: 'Per {cycleLabel}',
cycleLabelSuffix: '{cycleLabel} Pay',
infinityCycle: 'Perm',
// Server List
region: 'Region',
system: 'System',
config: 'Config',
};

129
src/config/i18n/zh-cn.js Normal file
View File

@ -0,0 +1,129 @@
/**
* 国际化配置
* 中文
*/
export default {
// 状态
online: '在线',
offline: '离线',
expired: '已过期',
// 通用
total: '共',
used: '使用',
remainingTime: '剩余',
// 服务器相关
server: '服务器',
servers: '{count}台',
serverTotal: '共{count}台服务器',
serverDetail: '节点详情',
mapPointServerLabel: '{name},{count}台',
// 登录和管理
login: '登录',
dashboard: '管理后台',
toDashboard: '访问管理后台',
loginDashboard: '登录管理后台',
card: '卡片',
list: '列表',
// 哪吒监控
nezhawiki: '哪吒监控',
nezhawikiVersion: '当前为哪吒监控',
searchPlaceholder: '可搜索服务器名称、标签、系统、国别代码',
// 网络相关
netSpeed: '网速',
bandwidth: '带宽',
inTransfer: '入网流量',
outTransfer: '出网流量',
inSpeed: '入网',
outSpeed: '出网',
speeds: '网速',
traffic: '流量',
trafficDouble: '双向流量',
trafficSingleOut: '单向出流量',
trafficSingleMax: '单向取最大流量',
trafficMaxOut: '最大出流量',
trafficMaxIn: '最大入流量',
ipv4: 'IPv4',
ipv6: 'IPv6',
ipv4ipv6: '双栈IP',
// 硬件资源
cpu: 'CPU',
gpu: 'GPU',
mem: '内存',
mems: '内存{memComputed}',
rams: '运行内存{ramComputed}',
swap: '交换',
swaps: '交换{swapComputed}',
swapMem: '交换内存{swapComputed}',
disk: '硬盘',
disks: '磁盘{diskComputed}',
diskCapacity: '磁盘容量{diskComputed}',
// 温度相关
temperature: '温度',
acpitz: '主板',
acpitzMean: '主板平均',
coretempPackage: 'CPU温度',
coretempCore: '核心平均',
coretempMaxCore: '最热核心',
// 系统状态
systemOS: '系统',
occupancy: '占用',
load: '负载',
conn: '连接',
boot: '启动',
active: '活跃',
processCount: '进程数',
tcp: 'TCP',
udp: 'UDP',
// 标签和计划
tag: '标签',
billPlan: '套餐',
// 网络监控
networkMonitor: '网络监控',
networkMonitorRefresh: '是否自动刷新',
refreshLabel: '刷新',
networkMonitorPeakShaving: '削峰',
networkMonitorPeakShavingTips: '过滤太高或太低的数据',
networkMonitorLast: '最近',
avgDelay: '平均延迟',
successRate: '成功率',
// 计费和订购
billing: '费用',
orderLink: '链接',
buyBtnText: '购买',
// 时间单位
day: '天',
days: '{count}天',
hour: '小时',
hours: '{count}小时',
minute: '分钟',
minutes: '{count}分钟',
// 计费周期
monthly: '月',
yearly: '年',
quarterly: '季',
halfYearly: '半年',
free: '免费',
payg: '按量',
cycleLabel: '每{cycleLabel}',
cycleLabelSuffix: '{cycleLabel}付',
infinityCycle: '长期有效',
// 服务器列表
region: '地区',
system: '系统',
config: '配置',
};

View File

@ -33,6 +33,13 @@ if (config.nazhua.nezhaVersion) {
config.init = true;
}
function setLocale() {
if (config.nazhua.locale) {
document.body.classList.add(`locale-${config.nazhua.locale}`);
}
}
setLocale();
function setColorMode() {
if (config.nazhua.simpleColorMode) {
document.body.classList.add('simple-color-mode');

22
src/i18n.js Normal file
View File

@ -0,0 +1,22 @@
/**
* 国际化处理
*/
import {
createI18n,
} from 'vue-i18n';
import config from '@/config';
import zhCN from './config/i18n/zh-cn';
import en from './config/i18n/en';
const i18n = createI18n({
legacy: false,
locale: config.nazhua.locale || 'zh-cn',
fallbackLocale: config.nazhua.locale || 'zh-cn',
messages: {
'zh-cn': zhCN,
en,
},
});
export default i18n;

View File

@ -5,7 +5,7 @@
<a
:href="dashboardUrl"
class="dashboard-url"
:title="userLogin ? '访问管理后台' : '登录管理后台'"
:title="userLogin ? $t('toDashboard') : $t('loginDashboard')"
target="_blank"
>
<span
@ -14,7 +14,7 @@
'ri-user-line': !userLogin,
}"
/>
<span>{{ userLogin ? '管理后台' : '登录' }}</span>
<span>{{ userLogin ? $t('dashboard') : $t('login') }}</span>
</a>
</div>
</template>

View File

@ -12,9 +12,9 @@
<a
ref="nofollow"
href="https://nezha.wiki"
:title="'当前为哪吒监控' + $config.nazhua.nezhaVersion"
:title="$t('nezhawikiVersion') + $config.nazhua.nezhaVersion"
target="_blank"
>哪吒监控</a>
>{{ $t('nezhawiki') }}</a>
</span>
<span class="text">
Theme By <a

View File

@ -16,7 +16,7 @@
ref="searchInputRef"
v-model.trim="searchWord"
type="text"
placeholder="可搜索服务器名称、标签、系统、国别代码"
:placeholder="$t('searchPlaceholder')"
class="search-box-input"
@input="onSearchInput"
@keydown.enter="onSearchInput"

View File

@ -4,21 +4,19 @@
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 class="text">{{ $t('serverTotal', { count: serverCount.total }) }}</span>
</span>
<template v-if="serverCount.online !== serverCount.total">
<span
class="server-count server-count--online"
>
<span class="text">在线</span>
<span class="text">{{ $t('online') }}</span>
<span class="value">{{ serverCount.online }}</span>
</span>
<span
class="server-count server-count--offline"
>
<span class="text">离线</span>
<span class="text">{{ $t('offline') }}</span>
<span class="value">{{ serverCount.offline }}</span>
</span>
</template>

View File

@ -8,7 +8,7 @@
class="server-stat server-stat--transfer"
>
<span class="server-stat-label">
<span class="text">流量</span>
<span class="text">{{ $t('traffic') }}</span>
</span>
<div class="server-stat-content">
<span class="server-stat-item server-stat-item--in">
@ -36,7 +36,7 @@
class="server-stat server-stat--net-speed"
>
<span class="server-stat-label">
<span class="text">网速</span>
<span class="text">{{ $t('netSpeed') }}</span>
</span>
<div class="server-stat-content">
<span class="server-stat-item server-stat-item--in">
@ -213,6 +213,11 @@ const serverStat = computed(() => {
line-height: 16px;
font-size: 12px;
.server-stat-label {
min-width: 2.5em;
text-transform: uppercase;
}
.server-stat-content {
flex: 1;
display: flex;

View File

@ -4,6 +4,7 @@ import {
createWebHashHistory,
} from 'vue-router';
import config from '@/config';
import i18n from '@/i18n';
import pageTitle from '@/utils/page-title';
const constantRoutes = [{
@ -15,7 +16,7 @@ const constantRoutes = [{
path: '/:serverId(\\d+)',
component: () => import('@/views/detail.vue'),
meta: {
title: '节点详情',
title: () => i18n.global.t('serverDetail'),
},
props: true,
}, {
@ -42,7 +43,8 @@ const router = createRouter(routerOptions);
router.beforeResolve((to, from, next) => {
if (to?.meta?.title) {
pageTitle(to?.meta?.title);
const title = typeof to.meta.title === 'function' ? to.meta.title() : to.meta.title;
pageTitle(title);
} else if (to.name === 'Home') {
pageTitle(config.nazhua.title);
}

View File

@ -1,3 +1,4 @@
import i18n from './i18n';
import './load';
import './assets/scss/base.scss';
import router from './router';
@ -8,6 +9,7 @@ import DotDotBox from './components/dot-dot-box.vue';
import Popover from './components/popover.vue';
export default (app) => {
app.use(i18n);
app.use(router);
app.use(store);
app.component('DotDotBox', DotDotBox);

View File

@ -2,7 +2,7 @@
<dot-dot-box class="server-info-box">
<div class="server-info-group server-info--cpu">
<div class="server-info-label">
CPU
{{ $t('cpu') }}
</div>
<div class="server-info-content">
<template v-if="info?.Host?.CPU?.length === 1">
@ -33,7 +33,7 @@
class="server-info-group server-info--gpu"
>
<div class="server-info-label">
GPU
{{ $t('gpu') }}
</div>
<div class="server-info-content">
<template v-if="gpuList.length === 1">
@ -64,7 +64,7 @@
class="server-info-group server-info--temperature"
>
<div class="server-info-label">
温度
{{ $t('temperature') }}
</div>
<div class="server-info-content">
<div class="server-info-item-group">
@ -93,7 +93,7 @@
</div>
<div class="server-info-group server-info--system-os">
<div class="server-info-label">
系统
{{ $t('systemOS') }}
</div>
<div class="server-info-content">
<span class="server-info-item">
@ -109,16 +109,16 @@
</div>
<div class="server-info-group server-info--load">
<div class="server-info-label">
占用
{{ $t('occupancy') }}
</div>
<div class="server-info-content">
<div class="server-info-item-group">
<span class="server-info-item process-count">
<span class="server-info-item-label">进程数</span>
<span class="server-info-item-label">{{ $t('processCount') }}</span>
<span class="server-info-item-value">{{ processCount }}</span>
</span>
<span class="server-info-item load">
<span class="server-info-item-label">负载</span>
<span class="server-info-item-label">{{ $t('load') }}</span>
<span class="server-info-item-value">
{{ sysLoadInfo }}
</span>
@ -128,19 +128,19 @@
</div>
<div class="server-info-group server-info--transfer">
<div class="server-info-label">
流量
{{ $t('traffic') }}
</div>
<div class="server-info-content">
<div class="server-info-item-group">
<span class="server-info-item transfer--in">
<span class="server-info-item-label">入网</span>
<span class="server-info-item-label">{{ $t('inTransfer') }}</span>
<span class="server-info-item-value">
<span class="text-value">{{ transfer?.in?.value }}</span>
<span class="text-unit">{{ transfer?.in?.unit }}</span>
</span>
</span>
<span class="server-info-item transfer--out">
<span class="server-info-item-label">出网</span>
<span class="server-info-item-label">{{ $t('outTransfer') }}</span>
<span class="server-info-item-value">
<span class="text-value">{{ transfer?.out?.value }}</span>
<span class="text-unit">{{ transfer?.out?.unit }}</span>
@ -151,16 +151,16 @@
</div>
<div class="server-info-group server-info--conn">
<div class="server-info-label">
连接
{{ $t('conn') }}
</div>
<div class="server-info-content">
<div class="server-info-item-group">
<span class="server-info-item conn--tcp">
<span class="server-info-item-label">TCP</span>
<span class="server-info-item-label">{{ $t('tcp') }}</span>
<span class="server-info-item-value">{{ tcpConnCount }}</span>
</span>
<span class="server-info-item conn--tcp">
<span class="server-info-item-label">UDP</span>
<span class="server-info-item-label">{{ $t('udp') }}</span>
<span class="server-info-item-value">{{ udpConnCount }}</span>
</span>
</div>
@ -168,7 +168,7 @@
</div>
<div class="server-info-group server-info--boottime">
<div class="server-info-label">
启动
{{ $t('boot') }}
</div>
<div class="server-info-content">
<span class="server-info-item runtime--boottime">
@ -178,7 +178,7 @@
</div>
<div class="server-info-group server-info--lasttime">
<div class="server-info-label">
活跃
{{ $t('active') }}
</div>
<div class="server-info-content">
<span class="server-info-item runtime--lasttime">
@ -191,7 +191,7 @@
class="server-info-group server-info--biil-plan"
>
<div class="server-info-label">
套餐
{{ $t('billPlan') }}
</div>
<div class="server-info-content">
<div class="server-info-item-group">
@ -214,7 +214,7 @@
class="server-info-group server-info--tag-list"
>
<div class="server-info-label">
标签
{{ $t('tag') }}
</div>
<div class="server-info-content">
<div class="server-info-tag-list">
@ -257,12 +257,17 @@
import {
computed,
} from 'vue';
import {
useI18n,
} from 'vue-i18n';
import dayjs from 'dayjs';
import config from '@/config';
import * as hostUtils from '@/utils/host';
import handleServerBillAndPlan from '@/views/composable/server-bill-and-plan';
const i18n = useI18n();
const props = defineProps({
info: {
type: Object,
@ -357,14 +362,14 @@ const temperatureData = computed(() => {
if (acpitz.length) {
data.push({
label: '主板',
label: i18n.t('acpitz'),
value: `${acpitz[0]}`,
type: 'acpitz',
});
if (acpitz.length) {
const acpitzMean = (acpitz.reduce((a, b) => a + b, 0) / acpitz.length).toFixed(1) * 1;
data.push({
label: '主板平均',
label: i18n.t('acpitzMean'),
value: `${acpitzMean}`,
title: acpitz.map((i, index) => `传感器${index + 1}: ${i}`).join('\n'),
type: 'acpitz-mean',
@ -373,7 +378,7 @@ const temperatureData = computed(() => {
}
if (coretemp_package_id.length) {
data.push({
label: 'CPU温度',
label: i18n.t('coretempPackage'),
value: coretemp_package_id.map((i) => i.value).join(', '),
title: coretemp_package_id.length > 1
? coretemp_package_id.map((i) => `CPU.${i.index + 1}: ${i.value}`).join('\n')
@ -384,7 +389,7 @@ const temperatureData = computed(() => {
if (coretemp_core.length) {
const coretempCoreMean = (coretemp_core.reduce((a, b) => a + b.value, 0) / coretemp_core.length).toFixed(1) * 1;
data.push({
label: '核心平均',
label: i18n.t('coretempCoreMean'),
value: `${coretempCoreMean}`,
title: coretemp_core.map((i) => `核心${i.index + 1}: ${i.value}`).join('\n'),
type: 'coretemp-core',
@ -401,19 +406,13 @@ const temperatureData = computed(() => {
// 20%
if (max / coretempCoreMean > 1.2) {
data.push({
label: `最热核心.${maxCore + 1}`,
label: `${i18n.t('coretempMaxCore')}.${maxCore + 1}`,
value: `${max}`,
type: 'coretemp-max-core',
});
}
}
if (other.length) {
// data.push({
// type: 'other',
// label: '',
// value: '...',
// title: other.map((i) => `${i.label}: ${i.value}`).join('\n'),
// });
other.forEach((i) => {
data.push({
label: i.label,
@ -459,11 +458,11 @@ const tagList = computed(() => {
list.push(...extra?.split?.(','));
}
if (IPv4 === '1' && IPv6 === '1') {
list.push('双栈IP');
list.push(i18n.t('ipv4ipv6'));
} else if (IPv4 === '1') {
list.push('仅IPv4');
list.push(i18n.t('ipv4'));
} else if (IPv6 === '1') {
list.push('仅IPv6');
list.push(i18n.t('ipv6'));
}
return list;
});
@ -541,6 +540,7 @@ const processCount = computed(() => props.info?.State?.ProcessCount);
text-align: center;
line-height: var(--server-info-item-size);
color: #ccc;
text-transform: uppercase;
}
.server-info-content {
@ -661,3 +661,11 @@ const processCount = computed(() => props.info?.State?.ProcessCount);
}
}
</style>
<style lang="scss">
body.locale-en {
.server-info-label {
text-align: left !important;
}
}
</style>

View File

@ -7,16 +7,16 @@
<div class="module-head-group">
<div class="left-box">
<span class="module-title">
网络监控
{{ $t('networkMonitor') }}
</span>
</div>
<div class="right-box">
<div
class="refresh-data-group"
title="是否自动刷新"
:title="$t('networkMonitorRefresh')"
@click="switchRefresh"
>
<span class="label-text">刷新</span>
<span class="label-text">{{ $t('refreshLabel') }}</span>
<div
class="switch-box"
:class="{
@ -28,10 +28,10 @@
</div>
<div
class="peak-shaving-group"
title="过滤太高或太低的数据"
:title="$t('networkMonitorPeakShavingTips')"
@click="switchPeakShaving"
>
<span class="label-text">削峰</span>
<span class="label-text">{{ $t('networkMonitorPeakShaving') }}</span>
<div
class="switch-box"
:class="{
@ -43,7 +43,7 @@
</div>
<div class="last-update-time-group">
<span class="last-update-time-label">
最近
{{ $t('networkMonitorLast') }}
</span>
<div class="minutes">
<div
@ -126,6 +126,9 @@ import {
onUnmounted,
} from 'vue';
import { useStore } from 'vuex';
import {
useI18n,
} from 'vue-i18n';
import config from '@/config';
import request from '@/utils/request';
import validate from '@/utils/validate';
@ -137,6 +140,8 @@ import {
getLineColor,
} from '@/views/composable/server-monitor';
const i18n = useI18n();
const props = defineProps({
info: {
type: Object,
@ -148,22 +153,22 @@ const store = useStore();
const minute = ref(1440);
const minutes = [{
label: '30分钟',
label: i18n.t('minutes', { count: 30 }),
value: 30,
}, {
label: '1小时',
label: i18n.t('hours', { count: 1 }),
value: 60,
}, {
label: '3小时',
label: i18n.t('hours', { count: 3 }),
value: 180,
}, {
label: '6小时',
label: i18n.t('hours', { count: 6 }),
value: 360,
}, {
label: '12小时',
label: i18n.t('hours', { count: 12 }),
value: 720,
}, {
label: '24小时',
label: i18n.t('hours', { count: 24 }),
value: 1440,
}];
const refreshData = ref(true);
@ -275,8 +280,8 @@ const monitorChartData = computed(() => {
}
const titles = [
cateItem.name,
cateItem.avg === 0 ? '' : `平均延迟:${cateItem.avg}ms`,
`成功率:${cateItem.over}%`,
cateItem.avg === 0 ? '' : `${i18n.t('avgDelay')}: ${cateItem.avg}ms`,
`${i18n.t('successRate')}: ${cateItem.over}%`,
];
cateItem.title = titles.filter((s) => s).join('\n');
cateList.push(cateItem);

View File

@ -78,11 +78,15 @@
import {
computed,
} from 'vue';
import {
useI18n,
} from 'vue-i18n';
import config from '@/config';
import handleServerBillAndPlan from '@/views/composable/server-bill-and-plan';
const i18n = useI18n();
const props = defineProps({
info: {
type: Object,
@ -130,11 +134,11 @@ const tagList = computed(() => {
list.push(...extra.split(','));
}
if (IPv4 === '1' && IPv6 === '1') {
list.push('双栈IP');
list.push(i18n.t('ipv4ipv6'));
} else if (IPv4 === '1') {
list.push('仅IPv4');
list.push(i18n.t('ipv4'));
} else if (IPv6 === '1') {
list.push('仅IPv6');
list.push(i18n.t('ipv6'));
}
// 5
return list.slice(0, 5);

View File

@ -2,19 +2,19 @@
<server-list-column
v-if="extraFields?.remainingTime"
prop="remaining-time"
label="剩余"
:label="$t('remainingTime')"
:value="billAndPlan?.remainingTime?.value || '-'"
/>
<server-list-column
v-if="extraFields?.billing"
prop="billing"
label="费用"
:label="$t('billing')"
:value="billAndPlan?.billing?.value || '-'"
/>
<server-list-column
v-if="extraFields?.orderLink"
prop="order-link"
label="链接"
:label="$t('orderLink')"
:slot-content="true"
>
<span
@ -36,6 +36,9 @@ import {
inject,
computed,
} from 'vue';
import {
useI18n,
} from 'vue-i18n';
import config from '@/config';
@ -43,6 +46,8 @@ import handleServerBillAndPlan from '@/views/composable/server-bill-and-plan';
import ServerListColumn from './server-list-column.vue';
const i18n = useI18n();
const props = defineProps({
info: {
type: Object,
@ -66,7 +71,7 @@ const buyBtnText = computed(() => {
if (props.info?.PublicNote?.customData?.buyBtnText) {
return props.info?.PublicNote?.customData?.buyBtnText;
}
return config.nazhua.buyBtnText || '购买';
return config.nazhua.buyBtnText || i18n.t('buy');
});
const showBuyBtn = computed(() => !!props.info?.PublicNote?.customData?.orderLink);

View File

@ -11,7 +11,7 @@
:used="item.used"
:colors="item.colors"
:val-text="item.valPercent"
:val-percent="`${item.label}使用${item.valText}`"
:val-percent="`${item.label}${$t('used')}${item.valText}`"
:label="item.label"
/>
</div>

View File

@ -28,17 +28,17 @@
</div>
<server-list-column
prop="server-flag"
label="地区"
:label="$t('region')"
:value="info?.Host?.CountryCode?.toUpperCase() || 'UN'"
/>
<server-list-column
prop="server-system"
label="系统"
:label="$t('system')"
:value="platformSystemLabel || '-'"
/>
<server-list-column
prop="cpu-mem"
label="配置"
:label="$t('config')"
:value="cpuAndMemAndDisk || '-'"
/>
<server-list-item-status

View File

@ -10,7 +10,7 @@
/>
<div
class="progress-bar-label"
:title="label + '使用' + used + '%'"
:title="label + $t('used') + used + '%'"
>
<span class="server-status-label">
{{ label }}:

View File

@ -2,6 +2,7 @@ import {
computed,
} from 'vue';
import dayjs from 'dayjs';
import i18n from '@/i18n';
import config from '@/config';
import validate from '@/utils/validate';
import * as dateUtils from '@/utils/date';
@ -36,7 +37,7 @@ export default (params) => {
case 'mo':
case 'month':
case 'monthly':
cycleLabel = '月';
cycleLabel = i18n.global.t('monthly');
months = 1;
break;
case '年':
@ -44,12 +45,12 @@ export default (params) => {
case 'yr':
case 'year':
case 'annual':
cycleLabel = '年';
cycleLabel = i18n.global.t('yearly');
months = 12;
break;
case '季':
case 'quarterly':
cycleLabel = '季';
cycleLabel = i18n.global.t('quarterly');
months = 3;
break;
case '半':
@ -57,7 +58,7 @@ export default (params) => {
case 'h':
case 'half':
case 'semi-annually':
cycleLabel = '半年';
cycleLabel = i18n.global.t('halfYearly');
months = 6;
break;
default:
@ -70,13 +71,13 @@ export default (params) => {
let amountValue = billingDataMod.amount;
let label;
if (billingDataMod.amount.toString() === '-1') {
amountValue = '按量';
label = cycleLabel ? `${cycleLabel}` : '';
amountValue = i18n.global.t('payg');
label = cycleLabel ? i18n.global.t('cycleLabel', { cycleLabel }) : '';
} else if (billingDataMod.amount.toString() === '0') {
amountValue = config.nazhua.freeAmount || '免费';
amountValue = config.nazhua.freeAmount || i18n.global.t('free');
isFree = true;
} else {
label = cycleLabel ? `${cycleLabel}` : '';
label = cycleLabel ? i18n.global.t('cycleLabelSuffix', { cycleLabel }) : '';
}
obj.billing = {
label,
@ -96,8 +97,8 @@ export default (params) => {
const endTime = dayjs(endDate).valueOf();
if (endDate.indexOf('0000-00-00') === 0) {
obj.remainingTime = {
label: '剩余',
value: config.nazhua.infinityCycle || '长期有效',
label: i18n.global.t('remaining'),
value: config.nazhua.infinityCycle || i18n.global.t('infinityCycle'),
type: 'infinity',
};
} else if (autoRenewal === '1') {
@ -106,8 +107,8 @@ export default (params) => {
if (endTime > nowTime) {
const diff = dayjs(endTime).diff(dayjs(), 'day') + 1;
obj.remainingTime = {
label: '剩余',
value: `${diff}`,
label: i18n.global.t('remaining'),
value: i18n.global.t('days', { count: diff }),
value2: diff,
type: 'autoRenewal-endTime',
};
@ -116,8 +117,8 @@ export default (params) => {
const nextTime = dateUtils.getNextCycleTime(endTime, months, nowTime);
const diff = dayjs(nextTime).diff(dayjs(), 'day') + 1;
obj.remainingTime = {
label: '剩余',
value: `${diff}`,
label: i18n.global.t('remaining'),
value: i18n.global.t('days', { count: diff }),
value2: diff,
type: 'autoRenewal-nextTime',
};
@ -125,15 +126,15 @@ export default (params) => {
} else if (endTime > nowTime) {
const diff = dayjs(endTime).diff(dayjs(), 'day') + 1;
obj.remainingTime = {
label: '剩余',
value: `${diff}`,
label: i18n.global.t('remaining'),
value: i18n.global.t('days', { count: diff }),
value2: diff,
type: 'endTime',
};
} else {
obj.remainingTime = {
label: '剩余',
value: '已过期',
label: i18n.global.t('remaining'),
value: i18n.global.t('expired'),
type: 'expired',
};
}
@ -142,19 +143,19 @@ export default (params) => {
if (planDataMod) {
if (planDataMod.bandwidth) {
obj.bandwidth = {
label: '带宽',
label: i18n.global.t('bandwidth'),
value: planDataMod.bandwidth,
};
}
if (planDataMod.trafficVol) {
let trafficTypeLabel = '双向';
let trafficTypeLabel = i18n.global.t('trafficDouble');
if (planDataMod.trafficType === '1') {
trafficTypeLabel = '单向出';
trafficTypeLabel = i18n.global.t('trafficSingleOut');
} else if (planDataMod.trafficType === '3') {
trafficTypeLabel = '单向取最大';
trafficTypeLabel = i18n.global.t('trafficSingleMax');
}
obj.traffic = {
label: `${trafficTypeLabel}流量`,
label: trafficTypeLabel,
value: planDataMod.trafficVol,
};
}

View File

@ -2,6 +2,7 @@ import {
computed,
} from 'vue';
import dayjs from 'dayjs';
import i18n from '@/i18n';
import validate from '@/utils/validate';
import * as dateUtils from '@/utils/date';
import * as hostUtils from '@/utils/host';
@ -79,7 +80,7 @@ export default (params) => {
let ruleStat;
ruleStat = total;
result.statType = 'Total';
result.statTypeLabel = '双向';
result.statTypeLabel = i18n.global.t('trafficDouble');
if (props.info?.PublicNote && validate.isSet(props.info.PublicNote?.planDataMod?.trafficType)) {
const {
trafficType = 2,
@ -88,17 +89,17 @@ export default (params) => {
case 1:
ruleStat = props.info.State.NetOutTransfer;
result.statType = 'Out';
result.statTypeLabel = '单向出';
result.statTypeLabel = i18n.global.t('trafficSingleOut');
break;
case 3:
if (props.info?.State?.NetOutTransfer >= props.info?.State?.NetInTransfer) {
ruleStat = props.info.State.NetOutTransfer;
result.statType = 'MaxOut';
result.statTypeLabel = '最大出';
result.statTypeLabel = i18n.global.t('trafficSingleMax');
} else if (props.info?.State?.NetOutTransfer < props.info?.State?.NetInTransfer) {
ruleStat = props.info.State.NetInTransfer;
result.statType = 'MaxIn';
result.statTypeLabel = '最大入';
result.statTypeLabel = i18n.global.t('trafficSingleMax');
}
break;
default:
@ -209,12 +210,12 @@ export default (params) => {
case 'duration':
return {
key,
label: '在线',
label: i18n.global.t('online'),
value: duration.value?.value,
unit: duration.value?.unit,
show: validate.isSet(duration.value?.value),
};
case 'transfer':
case 'traffic':
return {
key,
label: `${transfer.value.statTypeLabel}流量`,
@ -225,7 +226,7 @@ export default (params) => {
case 'inTransfer':
return {
key,
label: '入网流量',
label: i18n.global.t('inTransfer'),
value: inTransfer.value?.value,
unit: inTransfer.value?.unit,
show: validate.isSet(inTransfer.value?.value),
@ -233,7 +234,7 @@ export default (params) => {
case 'outTransfer':
return {
key,
label: '出网流量',
label: i18n.global.t('outTransfer'),
value: outTransfer.value?.value,
unit: outTransfer.value?.unit,
show: validate.isSet(outTransfer.value?.value),
@ -241,7 +242,7 @@ export default (params) => {
case 'inSpeed':
return {
key,
label: '入网',
label: i18n.global.t('inSpeed'),
value: netInSpeed.value?.value,
unit: netInSpeed.value?.unit,
show: validate.isSet(netInSpeed.value?.value),
@ -249,7 +250,7 @@ export default (params) => {
case 'outSpeed':
return {
key,
label: '出网',
label: i18n.global.t('outSpeed'),
value: netOutSpeed.value?.value,
unit: netOutSpeed.value?.unit,
show: validate.isSet(netOutSpeed.value?.value),
@ -257,18 +258,18 @@ export default (params) => {
case 'speeds':
return {
key,
label: '网速',
label: i18n.global.t('speeds'),
values: [
{
key: 'in',
label: '入网',
label: i18n.global.t('inSpeed'),
value: netInSpeed.value?.value,
unit: netInSpeed.value?.unit,
show: validate.isSet(netInSpeed.value?.value),
},
{
key: 'out',
label: '出网',
label: i18n.global.t('outSpeed'),
value: netOutSpeed.value?.value,
unit: netOutSpeed.value?.unit,
show: validate.isSet(netOutSpeed.value?.value),
@ -279,7 +280,7 @@ export default (params) => {
case 'load':
return {
key,
label: '负载',
label: i18n.global.t('load'),
value: (props.info.State?.Load1 || 0).toFixed(2),
unit: '',
show: validate.isSet(props.info.State?.Load1),

View File

@ -2,6 +2,7 @@ import {
computed,
} from 'vue';
import config from '@/config';
import i18n from '@/i18n';
import validate from '@/utils/validate';
import * as hostUtils from '@/utils/host';
@ -115,7 +116,7 @@ export default (params) => {
},
valText,
valPercent,
label: 'CPU',
label: i18n.global.t('cpu'),
content: {
default: cpuInfo.value?.core || CoresVal,
mobile: CoresVal,
@ -146,10 +147,14 @@ export default (params) => {
},
valText,
valPercent: `${useMemAndTotalMem.value.usePercent.toFixed(1) * 1}%`,
label: '内存',
label: i18n.global.t('mem'),
content: {
default: `运行内存${contentVal}`,
mobile: `内存${contentVal}`,
default: i18n.global.t('rams', {
ramComputed: contentVal,
}),
mobile: i18n.global.t('mems', {
memComputed: contentVal,
}),
},
};
}
@ -180,10 +185,14 @@ export default (params) => {
},
valText,
valPercent: `${useSwapAndTotalSwap.value.usePercent.toFixed(1) * 1}%`,
label: '交换',
label: i18n.global.t('swap'),
content: {
default: `交换内存${contentVal}`,
mobile: `交换${contentVal}`,
default: i18n.global.t('swapMem', {
swapComputed: contentVal,
}),
mobile: i18n.global.t('swaps', {
swapComputed: contentVal,
}),
},
};
}
@ -211,10 +220,14 @@ export default (params) => {
},
valText,
valPercent: `${useDiskAndTotalDisk.value.usePercent.toFixed(1) * 1}%`,
label: '磁盘',
label: i18n.global.t('disk'),
content: {
default: `磁盘容量${contentValue}`,
mobile: `磁盘${contentValue}`,
default: i18n.global.t('diskCapacity', {
diskComputed: contentValue,
}),
mobile: i18n.global.t('disks', {
diskComputed: contentValue,
}),
},
};
}

View File

@ -48,6 +48,9 @@ import {
import {
useRouter,
} from 'vue-router';
import {
useI18n,
} from 'vue-i18n';
import config from '@/config';
import {
@ -62,6 +65,8 @@ import ServerStatusBox from './components/server-detail/server-status-box.vue';
import ServerInfoBox from './components/server-detail/server-info-box.vue';
import ServerMonitor from './components/server-detail/server-monitor.vue';
const i18n = useI18n();
const props = defineProps({
serverId: {
type: [String, Number],
@ -132,7 +137,7 @@ function handleWorldMapWidth() {
watch(() => info.value, (oldValue, newValue) => {
if (!oldValue && newValue && router.currentRoute.value.name === 'ServerDetail') {
pageTitle(newValue?.Name, '节点详情');
pageTitle(newValue?.Name, i18n.t('serverDetail'));
handleWorldMapWidth();
}
});
@ -147,7 +152,7 @@ watch(() => dataInit.value, () => {
onMounted(() => {
if (info.value) {
pageTitle(info.value?.Name, '节点详情');
pageTitle(info.value?.Name, i18n.t('serverDetail'));
handleWorldMapWidth();
}
window.addEventListener('resize', handleWorldMapWidth);

View File

@ -84,6 +84,9 @@ import {
import {
useStore,
} from 'vuex';
import {
useI18n,
} from 'vue-i18n';
import config from '@/config';
import {
@ -100,7 +103,9 @@ import ServerListWarp from './components/server-list/server-list-warp.vue';
import ServerCardItem from './components/server-list/card/server-list-item.vue';
import ServerRowItem from './components/server-list/row/server-list-item.vue';
const i18n = useI18n();
const store = useStore();
const worldMapWidth = ref();
const windowWidth = ref(window.innerWidth);
@ -163,21 +168,21 @@ const tagOptions = computed(() => store.state.serverGroup.map((i) => ({
key: uuid(),
label: i.name,
value: i.name,
title: `${i.servers.length}`,
title: i18n.t('servers', { count: i.servers.length }),
})));
const onlineOptions = computed(() => {
if (serverCount.value?.total !== serverCount.value?.online) {
return [{
key: 'online',
label: '在线',
label: i18n.t('online'),
value: '1',
title: `${serverCount.value.online}`,
title: i18n.t('servers', { count: serverCount.value.online }),
}, {
key: 'offline',
label: '离线',
label: i18n.t('offline'),
value: '-1',
title: `${serverCount.value.offline}`,
title: i18n.t('servers', { count: serverCount.value.offline }),
}];
}
return [];
@ -185,12 +190,12 @@ const onlineOptions = computed(() => {
const listTypeOptions = computed(() => [{
key: 'card',
label: '卡片',
label: i18n.t('card'),
value: 'card',
icon: 'ri-gallery-view-2',
}, {
key: 'row',
label: '列表',
label: i18n.t('list'),
value: 'row',
icon: 'ri-list-view',
}]);
@ -281,7 +286,10 @@ const serverLocations = computed(() => {
y,
code,
size: count2size(servers.length),
label: `${name},${servers.length}`,
label: i18n.t('mapPointServerLabel', {
name,
count: servers.length,
}),
servers,
});
}

3097
yarn.lock

File diff suppressed because it is too large Load Diff