mirror of
https://github.com/woodchen-ink/nezha-dash-v1.git
synced 2025-07-18 01:21:56 +08:00
Merge branch 'main' of https://github.com/hamster1963/nezha-dash-v1 into hamster1963-main
This commit is contained in:
commit
b3df24ea08
19
.cert/cert.pem
Normal file
19
.cert/cert.pem
Normal file
@ -0,0 +1,19 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDCTCCAfGgAwIBAgIUQxY5HJAktPoEWU9osMraUrm/DEAwDQYJKoZIhvcNAQEL
|
||||
BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI1MDIxMzAzMTA0MVoXDTI2MDIx
|
||||
MzAzMTA0MVowFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF
|
||||
AAOCAQ8AMIIBCgKCAQEAraDt2UXkzKLRskNtVDo1iXe1tBTYTAFtl+m7JOvdYdmS
|
||||
oenV3Cn/8Cd8JuusQVl9jovcMFb3pwrQzodSQ9oN70B/MSqA/Pjgpji+uu4Hjcas
|
||||
VhaAHregBsV8ULl+OikPPFWcGKRZMtRyta3Sy/2E5Y44wr8vdERKDl/6ydDVioe5
|
||||
dQQS+klyzamy9ayQj8fpSTR96H+WpDd6gGuDf+XlrqlnrgatiUIJiDkeJPCIUNJi
|
||||
VSw8lq3KO8O4K376smCAdngdyYg+q/Sk2r5MnHi9VqNknwmos06yPk6vTWIpZ+mK
|
||||
bz9W2HW4sukU0nwRXP0p29SKoW5ZKPvrLvfNDp0P3QIDAQABo1MwUTAdBgNVHQ4E
|
||||
FgQUYSHtj6LjfaQ0BmuCdlHf/EXKm5AwHwYDVR0jBBgwFoAUYSHtj6LjfaQ0BmuC
|
||||
dlHf/EXKm5AwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAQnM/
|
||||
MIYunEp8ITMtllILW9TJhZVertfuux4S1rgRZ3VADmHgHftCgUKpm4kh8w2gEZ0M
|
||||
DXTmnIwqaBa+lpiCcALECUQ1L2jPcwCYowmEfnKLF6Ob3Tnznz0eqr8TnvuKCX4c
|
||||
ehSlfqOcUn8rveLDX91j+FJ+LSggf/kYjhE0ACtZHJyEM9csWu5chu8cCjpq5pn/
|
||||
ahiPw5eUnxsyBWdqlkMvY+lofH7SaunXrbLcIDg67wMl0FpZ39z/UAhIVNiyUIDe
|
||||
k7pNzRu99r5hIqdyfx5zULG2mzJCSsJj63t4BeDwr6u+zXSlyVMqh5cXj9mk4LJ6
|
||||
DhJlnudcCV5t/RGyOw==
|
||||
-----END CERTIFICATE-----
|
28
.cert/key.pem
Normal file
28
.cert/key.pem
Normal file
@ -0,0 +1,28 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCtoO3ZReTMotGy
|
||||
Q21UOjWJd7W0FNhMAW2X6bsk691h2ZKh6dXcKf/wJ3wm66xBWX2Oi9wwVvenCtDO
|
||||
h1JD2g3vQH8xKoD8+OCmOL667geNxqxWFoAet6AGxXxQuX46KQ88VZwYpFky1HK1
|
||||
rdLL/YTljjjCvy90REoOX/rJ0NWKh7l1BBL6SXLNqbL1rJCPx+lJNH3of5akN3qA
|
||||
a4N/5eWuqWeuBq2JQgmIOR4k8IhQ0mJVLDyWrco7w7grfvqyYIB2eB3JiD6r9KTa
|
||||
vkyceL1Wo2SfCaizTrI+Tq9NYiln6YpvP1bYdbiy6RTSfBFc/Snb1Iqhblko++su
|
||||
980OnQ/dAgMBAAECggEAI/6N+GI9N7AUVUaVqmWj1iL/Q/0jRwRvxhOyFIoiG6gp
|
||||
dg/+IhWB5bUlz4LBc8270fqME+hfkF1VYs9aXk8c3unJxHVJhsgIeGUgoyt33Owg
|
||||
K3ugJV4PWoAD0M9Xi/KZojokMVaW2EsDGcdWgSwGKjmk6jiMu6dxi8/Zc4+ryTsY
|
||||
3+KMUocFyqMfYK1/sYSTPzlPWcCGMuaO36Df++cAzKLlqHRh7BLgSiCXBrV8ITFf
|
||||
LTkQFDf/c+yVC6mJG/GXzqdKXS3OT97sW34tdmQPNhReCSkSEDVQt+tnFa5be1R2
|
||||
18mODkaSv4DxnMXnlfexon/pGuXukgrMTZQXq2+pIQKBgQDaNvBmJrSCU9NSDNSj
|
||||
I0yTX8DUzEv1bxErbfptSlSoUcEIPcLsxt+xZFVfU8IcMvQ43gHSsRquCfZsUZ0r
|
||||
/ZIfJ7pWTqbxd/EybsMiC2ZSS8NdVX3MJhKinrZXMTRAA8l5a4AFr2YKMtNqQpGY
|
||||
xWu8TS7PR8N9B6vZqGC9hhID4QKBgQDLsZLrezomWTthFAOfACj/ebIEyZ30YVNw
|
||||
7IaaVTkeWtYGJXasMrts1+n15dPwR6a18c65hSywJKsCEYD6z/uXxaoX1bK32oLw
|
||||
49thMw+qSilA1jMQ/XQxx9TFsmrCvwSm5xIjSV+0pD1sApiivGQAU+2oHZeEwLue
|
||||
v51JxnaLfQKBgQDVYUWgThbTHk8U+7DuObVGoyp3q7JXNJ1wf2GTf0zbLt54RZSX
|
||||
Xj0dRMRqrAey9Wx1MzpLIZ26M8nAz+nGO3Woe3utq8l5c9TqgP7VCpqqvKU0XkXd
|
||||
3Bj65gHdryKtukZIMgOFC6fXLy4mySOAZQRdpIeybzVMzLSR6SF4EmMJYQKBgGtz
|
||||
xVlLrCVGtThE4pQh9X6vp+U2poigPvA3FdqcUoFc0cJ0SOIV8SE91UHOd7stURhx
|
||||
8ueTBTv2W++/ZBbrWIF72HqyVJEASErjKHtiAEWI0bJOTKoNyhnonKmdsQwC0GVr
|
||||
R/otXrtgWLZ9uB9A2lAB9kDVO3TgZxkbY9HjS+3RAoGBAKVKcJFErNZhQxCx5ll3
|
||||
u9wtE7duiVcS3jZhFa7tvcSc4O5+ahEQG/gy2M6kgqB/f3nMH6Rd9wsTzwPp1uZz
|
||||
qiumr3ZOvpTWuLiIMQi3sE9pBGz7p+ZTeP8Z0Wez98v9MVmgsCsPqDOpa1JhnJIq
|
||||
2AgG3D/RUJylOPYnMq8vdAyx
|
||||
-----END PRIVATE KEY-----
|
14
index.html
14
index.html
@ -107,9 +107,17 @@
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/png" href="/apple-touch-icon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>CZL SVR</title>
|
||||
<link rel="stylesheet" href="https://i-aws.czl.net/jsdelivr/gh/lipis/flag-icons@7.0.0/css/flag-icons.min.css" />
|
||||
<link rel="stylesheet" href="https://i-aws.czl.net/jsdelivr/npm/font-logos@1/assets/font-logos.css" />
|
||||
<title>CZL Server</title>
|
||||
|
||||
<!-- PWA -->
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
<meta name="theme-color" content="hsl(0 0% 98%)" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
|
||||
<meta name="apple-mobile-web-app-title" content="CZL Server" />
|
||||
|
||||
<link rel="stylesheet" href="https://i.czl.net/jsdelivr/gh/lipis/flag-icons@7.0.0/css/flag-icons.min.css" />
|
||||
<link rel="stylesheet" href="https://i.czl.net/jsdelivr/npm/font-logos@1/assets/font-logos.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
72
package.json
72
package.json
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "nazha-dashboard-vite",
|
||||
"name": "nazha-dash-v1",
|
||||
"private": true,
|
||||
"version": "1.0.1",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
@ -13,58 +13,60 @@
|
||||
"@fontsource/inter": "^5.1.1",
|
||||
"@heroicons/react": "^2.2.0",
|
||||
"@number-flow/react": "^0.5.5",
|
||||
"@radix-ui/react-accordion": "^1.2.2",
|
||||
"@radix-ui/react-checkbox": "^1.1.3",
|
||||
"@radix-ui/react-dialog": "^1.0.5",
|
||||
"@radix-ui/react-dropdown-menu": "^2.0.6",
|
||||
"@radix-ui/react-label": "^2.0.2",
|
||||
"@radix-ui/react-popover": "^1.0.7",
|
||||
"@radix-ui/react-progress": "^1.1.1",
|
||||
"@radix-ui/react-separator": "^1.1.1",
|
||||
"@radix-ui/react-slot": "^1.1.1",
|
||||
"@radix-ui/react-switch": "^1.1.2",
|
||||
"@tanstack/react-query": "^5.65.1",
|
||||
"@tanstack/react-query-devtools": "^5.65.1",
|
||||
"@tanstack/react-table": "^8.20.6",
|
||||
"@radix-ui/react-accordion": "^1.2.3",
|
||||
"@radix-ui/react-checkbox": "^1.1.4",
|
||||
"@radix-ui/react-dialog": "^1.1.6",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.6",
|
||||
"@radix-ui/react-label": "^2.1.2",
|
||||
"@radix-ui/react-popover": "^1.1.6",
|
||||
"@radix-ui/react-progress": "^1.1.2",
|
||||
"@radix-ui/react-select": "^2.1.6",
|
||||
"@radix-ui/react-separator": "^1.1.2",
|
||||
"@radix-ui/react-slot": "^1.1.2",
|
||||
"@radix-ui/react-switch": "^1.1.3",
|
||||
"@radix-ui/react-tooltip": "^1.1.8",
|
||||
"@tanstack/react-query": "^5.66.7",
|
||||
"@tanstack/react-query-devtools": "^5.66.7",
|
||||
"@tanstack/react-table": "^8.21.2",
|
||||
"@trivago/prettier-plugin-sort-imports": "^5.2.2",
|
||||
"@types/d3-geo": "^3.1.0",
|
||||
"@types/luxon": "^3.4.2",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.0.0",
|
||||
"cmdk": "^0.2.0",
|
||||
"country-flag-icons": "^1.5.14",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "1.0.0",
|
||||
"country-flag-icons": "^1.5.18",
|
||||
"d3-geo": "^3.1.1",
|
||||
"dayjs": "^1.11.13",
|
||||
"framer-motion": "^12.0.6",
|
||||
"i18n-iso-countries": "^7.13.0",
|
||||
"framer-motion": "^12.4.5",
|
||||
"i18n-iso-countries": "^7.14.0",
|
||||
"i18next": "^24.2.2",
|
||||
"lucide-react": "^0.460.0",
|
||||
"luxon": "^3.5.0",
|
||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-i18next": "^15.4.0",
|
||||
"react-router-dom": "^7.1.3",
|
||||
"react-i18next": "^15.4.1",
|
||||
"react-router-dom": "^7.2.0",
|
||||
"recharts": "^2.15.1",
|
||||
"sonner": "^1.7.3",
|
||||
"sonner": "^1.7.4",
|
||||
"tailwind-merge": "^2.6.0",
|
||||
"tailwindcss-animate": "^1.0.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.19.0",
|
||||
"@types/node": "^22.12.0",
|
||||
"@types/react": "^19.0.8",
|
||||
"@types/react-dom": "^19.0.3",
|
||||
"@vitejs/plugin-react-swc": "^3.7.2",
|
||||
"@eslint/js": "^9.20.0",
|
||||
"@types/node": "^22.13.4",
|
||||
"@types/react": "^19.0.10",
|
||||
"@types/react-dom": "^19.0.4",
|
||||
"@vitejs/plugin-react-swc": "^3.8.0",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"eslint": "^9.19.0",
|
||||
"eslint": "^9.20.1",
|
||||
"eslint-plugin-react-hooks": "^5.1.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.18",
|
||||
"globals": "^15.14.0",
|
||||
"postcss": "^8.5.1",
|
||||
"eslint-plugin-react-refresh": "^0.4.19",
|
||||
"globals": "^15.15.0",
|
||||
"postcss": "^8.5.3",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"typescript": "~5.6.3",
|
||||
"typescript-eslint": "^8.22.0",
|
||||
"vite": "^6.0.11"
|
||||
"typescript-eslint": "^8.24.1",
|
||||
"vite": "^6.1.1"
|
||||
}
|
||||
}
|
||||
|
BIN
public/android-chrome-192x192.png
Normal file
BIN
public/android-chrome-192x192.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.7 KiB |
17
public/manifest.json
Normal file
17
public/manifest.json
Normal file
@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "Nezha Monitoring",
|
||||
"short_name": "Nezha Monitoring",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/android-chrome-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
}
|
||||
],
|
||||
"theme_color": "hsl(0 0% 98%)",
|
||||
"background_color": "hsl(0 0% 98%)",
|
||||
"start_url": "/",
|
||||
"display": "standalone",
|
||||
"orientation": "portrait"
|
||||
}
|
@ -17,7 +17,7 @@ export const CycleTransferStatsCard: React.FC<CycleTransferStatsProps> = ({ serv
|
||||
const serverIdList = serverList.map((server) => server.id.toString())
|
||||
|
||||
return (
|
||||
<section className="grid grid-cols-1 md:grid-cols-2 gap-2 md:gap-4">
|
||||
<section className="grid grid-cols-1 md:grid-cols-3 gap-3">
|
||||
{Object.entries(cycleStats).map(([cycleId, cycleData]) => {
|
||||
if (!cycleData.server_name) {
|
||||
return null
|
||||
|
@ -1,11 +1,8 @@
|
||||
import { formatBytes } from "@/lib/format"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { CircleStackIcon } from "@heroicons/react/24/outline"
|
||||
import React from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
import AnimatedCircularProgressBar from "./ui/animated-circular-progress-bar"
|
||||
|
||||
interface CycleTransferStatsClientProps {
|
||||
name: string
|
||||
from: string
|
||||
@ -26,7 +23,7 @@ export const CycleTransferStatsClient: React.FC<CycleTransferStatsClientProps> =
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"w-full bg-white px-4 py-3 rounded-lg border bg-card text-card-foreground shadow-lg shadow-neutral-200/40 dark:shadow-none space-y-2",
|
||||
"w-full bg-white px-4 py-3.5 rounded-lg border bg-card text-card-foreground hover:shadow-sm transition-all duration-200 dark:shadow-none",
|
||||
className,
|
||||
{
|
||||
"bg-card/70": customBackgroundImage,
|
||||
@ -37,43 +34,41 @@ export const CycleTransferStatsClient: React.FC<CycleTransferStatsClientProps> =
|
||||
const progress = (transfer / max) * 100
|
||||
|
||||
return (
|
||||
<div key={serverId}>
|
||||
<section className="flex justify-between items-center">
|
||||
<div className="bg-green-600 w-fit text-white px-1.5 py-0.5 rounded-full text-[10px]">{name}</div>
|
||||
<span className="text-stone-600 dark:text-stone-400 text-xs">
|
||||
{new Date(from).toLocaleDateString()} - {new Date(to).toLocaleDateString()}
|
||||
</span>
|
||||
</section>
|
||||
|
||||
<section className="flex justify-between items-center mt-2">
|
||||
<div className="flex gap-1 items-center">
|
||||
<CircleStackIcon className="size-3 text-neutral-400 dark:text-neutral-600" />
|
||||
<span className="text-sm font-semibold">{serverName}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<p className="text-xs text-end w-10 font-medium">{progress.toFixed(0)}%</p>
|
||||
<AnimatedCircularProgressBar className="size-4 text-[0px]" max={100} min={0} value={progress} primaryColor="hsl(var(--chart-5))" />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div className="w-full bg-neutral-100 dark:bg-neutral-800 rounded-full overflow-hidden h-2.5 mt-2">
|
||||
<div className="bg-green-600 h-2.5 rounded-full" style={{ width: `${Math.min(progress, 100)}%` }} />
|
||||
<div key={serverId} className="space-y-3">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm font-medium text-neutral-800 dark:text-neutral-200">{serverName}</span>
|
||||
<div className="bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 px-2 py-0.5 rounded text-xs font-medium">{name}</div>
|
||||
</div>
|
||||
|
||||
<section className="flex justify-between items-center mt-2">
|
||||
<span className="text-[13px] text-stone-800 dark:text-stone-400 font-medium">
|
||||
{formatBytes(transfer)} {t("cycleTransfer.used")}
|
||||
</span>
|
||||
<span className="text-xs text-stone-500 dark:text-stone-400 font-normal">
|
||||
{formatBytes(max)} {t("cycleTransfer.total")}
|
||||
</span>
|
||||
</section>
|
||||
|
||||
<section className="flex justify-between items-center mt-2">
|
||||
<div className="text-xs text-stone-500 dark:text-stone-400">
|
||||
{t("cycleTransfer.nextUpdate")}: {new Date(nextUpdate).toLocaleString()}
|
||||
{/* Progress Section */}
|
||||
<div className="space-y-1.5">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-baseline gap-1">
|
||||
<span className="text-sm font-medium text-neutral-800 dark:text-neutral-200">{formatBytes(transfer)}</span>
|
||||
<span className="text-xs text-neutral-500 dark:text-neutral-400">/ {formatBytes(max)}</span>
|
||||
</div>
|
||||
<span className="text-xs font-medium text-neutral-600 dark:text-neutral-300">{progress.toFixed(1)}%</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div className="relative h-1.5">
|
||||
<div className="absolute inset-0 bg-neutral-100 dark:bg-neutral-800 rounded-full" />
|
||||
<div
|
||||
className="absolute inset-0 bg-emerald-500 rounded-full transition-all duration-300"
|
||||
style={{ width: `${Math.min(progress, 100)}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="flex items-center justify-between text-[11px] text-neutral-500 dark:text-neutral-400">
|
||||
<span>
|
||||
{new Date(from).toLocaleDateString()} - {new Date(to).toLocaleDateString()}
|
||||
</span>
|
||||
<span>
|
||||
{t("cycleTransfer.nextUpdate")}: {new Date(nextUpdate).toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
|
@ -1,6 +1,16 @@
|
||||
import React from "react"
|
||||
|
||||
const Footer: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const isMac = /macintosh|mac os x/i.test(navigator.userAgent)
|
||||
|
||||
const { data: settingData } = useQuery({
|
||||
queryKey: ["setting"],
|
||||
queryFn: () => fetchSetting(),
|
||||
refetchOnMount: true,
|
||||
refetchOnWindowFocus: true,
|
||||
})
|
||||
|
||||
return (
|
||||
<footer className="mx-auto w-full max-w-5xl px-4 lg:px-0 pb-4 server-footer">
|
||||
<section className="flex flex-col">
|
||||
@ -11,9 +21,16 @@ const Footer: React.FC = () => {
|
||||
CZL LTD
|
||||
</a>
|
||||
</div>
|
||||
<p className="server-footer-theme">
|
||||
<div className="server-footer-theme flex flex-col items-center sm:items-end">
|
||||
<p className="mt-1 text-[13px] font-light tracking-tight text-neutral-600/50 dark:text-neutral-300/50">
|
||||
<kbd className="pointer-events-none mx-1 inline-flex h-4 select-none items-center gap-1 rounded border bg-muted px-1.5 font-mono text-[10px] font-medium text-muted-foreground opacity-100">
|
||||
<span className="text-xs">⌘</span>K
|
||||
</kbd>
|
||||
</p>
|
||||
<p className="server-footer-theme">
|
||||
All Rights Reserved
|
||||
</p>
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
</footer>
|
||||
|
@ -293,7 +293,7 @@ function Overview() {
|
||||
<div style={{ fontVariantNumeric: "tabular-nums" }} className="flex text-sm font-medium mt-0.5">
|
||||
<NumberFlow trend={1} value={time.hh} format={{ minimumIntegerDigits: 2 }} />
|
||||
<NumberFlow prefix=":" trend={1} value={time.mm} digits={{ 1: { max: 5 } }} format={{ minimumIntegerDigits: 2 }} />
|
||||
<NumberFlow prefix=":" trend={1} value={time.ss} digits={{ 1: { max: 5 } }} format={{ minimumIntegerDigits: 2 }} />
|
||||
<p className="mt-[0.5px]">:{time.ss.toString().padStart(2, "0")}</p>
|
||||
</div>
|
||||
</NumberFlowGroup>
|
||||
</div>
|
||||
|
@ -4,7 +4,6 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/com
|
||||
import { ChartConfig, ChartContainer, ChartLegend, ChartLegendContent, ChartTooltip, ChartTooltipContent } from "@/components/ui/chart"
|
||||
import { fetchMonitor } from "@/lib/nezha-api"
|
||||
import { cn, formatTime } from "@/lib/utils"
|
||||
import { formatRelativeTime } from "@/lib/utils"
|
||||
import { NezhaMonitor, ServerMonitorChart } from "@/types/nezha-api"
|
||||
import { useQuery } from "@tanstack/react-query"
|
||||
import * as React from "react"
|
||||
@ -95,8 +94,10 @@ export const NetworkChartClient = React.memo(function NetworkChart({
|
||||
|
||||
const customBackgroundImage = (window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined
|
||||
|
||||
const forcePeakCutEnabled = (window.ForcePeakCutEnabled as boolean) ?? false
|
||||
|
||||
const [activeChart, setActiveChart] = React.useState(defaultChart)
|
||||
const [isPeakEnabled, setIsPeakEnabled] = React.useState(false)
|
||||
const [isPeakEnabled, setIsPeakEnabled] = React.useState(forcePeakCutEnabled)
|
||||
|
||||
const handleButtonClick = useCallback(
|
||||
(chart: string) => {
|
||||
@ -264,12 +265,36 @@ export const NetworkChartClient = React.memo(function NetworkChart({
|
||||
<CartesianGrid vertical={false} />
|
||||
<XAxis
|
||||
dataKey="created_at"
|
||||
tickLine={false}
|
||||
tickLine={true}
|
||||
tickSize={3}
|
||||
axisLine={false}
|
||||
tickMargin={8}
|
||||
minTickGap={32}
|
||||
interval={"preserveStartEnd"}
|
||||
tickFormatter={(value) => formatRelativeTime(value)}
|
||||
minTickGap={80}
|
||||
ticks={processedData
|
||||
.filter((item, index, array) => {
|
||||
if (array.length < 6) {
|
||||
return index === 0 || index === array.length - 1
|
||||
}
|
||||
|
||||
// 计算数据的总时间跨度(毫秒)
|
||||
const timeSpan = array[array.length - 1].created_at - array[0].created_at
|
||||
const hours = timeSpan / (1000 * 60 * 60)
|
||||
|
||||
// 根据时间跨度调整显示间隔
|
||||
if (hours <= 12) {
|
||||
// 12小时内,每60分钟显示一个刻度
|
||||
return index === 0 || index === array.length - 1 || new Date(item.created_at).getMinutes() % 60 === 0
|
||||
}
|
||||
// 超过12小时,每2小时显示一个刻度
|
||||
const date = new Date(item.created_at)
|
||||
return date.getMinutes() === 0 && date.getHours() % 2 === 0
|
||||
})
|
||||
.map((item) => item.created_at)}
|
||||
tickFormatter={(value) => {
|
||||
const date = new Date(value)
|
||||
const minutes = date.getMinutes()
|
||||
return minutes === 0 ? `${date.getHours()}:00` : `${date.getHours()}:${minutes}`
|
||||
}}
|
||||
/>
|
||||
<YAxis tickLine={false} axisLine={false} tickMargin={15} minTickGap={20} tickFormatter={(value) => `${value}ms`} />
|
||||
<ChartTooltip
|
||||
|
@ -14,6 +14,7 @@ import { useTranslation } from "react-i18next"
|
||||
import { useNavigate } from "react-router-dom"
|
||||
|
||||
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "./ui/accordion"
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./ui/tooltip"
|
||||
|
||||
export default function ServerDetailOverview({ server_id }: { server_id: string }) {
|
||||
const { t } = useTranslation()
|
||||
@ -165,17 +166,26 @@ export default function ServerDetailOverview({ server_id }: { server_id: string
|
||||
) : null}
|
||||
|
||||
{country_code && (
|
||||
<Card className="rounded-[10px] bg-transparent border-none shadow-none">
|
||||
<CardContent className="px-1.5 py-1">
|
||||
<section className="flex flex-col items-start gap-0.5">
|
||||
<p className="text-xs text-muted-foreground">{t("serverDetail.region")}</p>
|
||||
<section className="flex items-start gap-1">
|
||||
<div className="text-xs text-start">{countries.getName(country_code?.toUpperCase(), "en")}</div>
|
||||
{country_code && <ServerFlag className="text-[11px] -mt-[1px]" country_code={country_code} />}
|
||||
</section>
|
||||
</section>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<TooltipProvider delayDuration={100}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Card className="rounded-[10px] bg-transparent border-none shadow-none">
|
||||
<CardContent className="px-1.5 py-1">
|
||||
<section className="flex flex-col items-start gap-0.5">
|
||||
<p className="text-xs text-muted-foreground">{t("serverDetail.region")}</p>
|
||||
<section className="flex items-start gap-1">
|
||||
<div className="text-xs text-start">{country_code?.toUpperCase()}</div>
|
||||
{country_code && <ServerFlag className="text-[11px] -mt-[1px]" country_code={country_code} />}
|
||||
</section>
|
||||
</section>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{countries.getName(country_code?.toUpperCase(), "en")}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
</section>
|
||||
<section className="flex flex-wrap gap-2 mt-1">
|
||||
|
@ -19,10 +19,16 @@ export function ServiceTracker({ serverList }: { serverList: NezhaServer[] }) {
|
||||
})
|
||||
|
||||
const processServiceData = (serviceData: ServiceData) => {
|
||||
const days = serviceData.up.map((up, index) => ({
|
||||
completed: up > serviceData.down[index],
|
||||
date: new Date(Date.now() - (29 - index) * 24 * 60 * 60 * 1000),
|
||||
}))
|
||||
const days = serviceData.up.map((up, index) => {
|
||||
const totalChecks = up + serviceData.down[index]
|
||||
const dailyUptime = totalChecks > 0 ? (up / totalChecks) * 100 : 0
|
||||
return {
|
||||
completed: up > serviceData.down[index],
|
||||
date: new Date(Date.now() - (29 - index) * 24 * 60 * 60 * 1000),
|
||||
uptime: dailyUptime,
|
||||
delay: serviceData.delay[index] || 0,
|
||||
}
|
||||
})
|
||||
|
||||
const totalUp = serviceData.up.reduce((a, b) => a + b, 0)
|
||||
const totalChecks = serviceData.up.reduce((a, b) => a + b, 0) + serviceData.down.reduce((a, b) => a + b, 0)
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"
|
||||
import { cn } from "@/lib/utils"
|
||||
import React from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
@ -8,6 +9,8 @@ interface ServiceTrackerProps {
|
||||
days: Array<{
|
||||
completed: boolean
|
||||
date?: Date
|
||||
uptime: number
|
||||
delay: number
|
||||
}>
|
||||
className?: string
|
||||
title?: string
|
||||
@ -18,6 +21,25 @@ interface ServiceTrackerProps {
|
||||
export const ServiceTrackerClient: React.FC<ServiceTrackerProps> = ({ days, className, title, uptime = 100, avgDelay = 0 }) => {
|
||||
const { t } = useTranslation()
|
||||
const customBackgroundImage = (window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined
|
||||
|
||||
const getUptimeColor = (uptime: number) => {
|
||||
if (uptime >= 99) return "text-emerald-500"
|
||||
if (uptime >= 95) return "text-amber-500"
|
||||
return "text-rose-500"
|
||||
}
|
||||
|
||||
const getDelayColor = (delay: number) => {
|
||||
if (delay < 100) return "text-emerald-500"
|
||||
if (delay < 300) return "text-amber-500"
|
||||
return "text-rose-500"
|
||||
}
|
||||
|
||||
const getStatusColor = (uptime: number) => {
|
||||
if (uptime >= 99) return "bg-emerald-500"
|
||||
if (uptime >= 95) return "bg-amber-500"
|
||||
return "bg-rose-500"
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
@ -30,27 +52,58 @@ export const ServiceTrackerClient: React.FC<ServiceTrackerProps> = ({ days, clas
|
||||
>
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-5 h-5 rounded-full bg-green-600 flex items-center justify-center">
|
||||
<div className="w-3 h-3 rounded-full bg-white dark:bg-black" />
|
||||
</div>
|
||||
<div className={cn("w-2.5 h-2.5 rounded-full transition-colors", getStatusColor(uptime))} />
|
||||
<span className="font-medium text-sm">{title}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-stone-600 dark:text-stone-400 font-medium text-sm">{avgDelay.toFixed(0)}ms</span>
|
||||
<Separator className="h-4 mx-0" orientation="vertical" />
|
||||
<span className="text-green-600 font-medium text-sm">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className={cn("font-medium text-sm transition-colors", getDelayColor(avgDelay))}>{avgDelay.toFixed(0)}ms</span>
|
||||
<Separator className="h-4" orientation="vertical" />
|
||||
<span className={cn("font-medium text-sm transition-colors", getUptimeColor(uptime))}>
|
||||
{uptime.toFixed(1)}% {t("serviceTracker.uptime")}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-[2px]">
|
||||
<div className="flex gap-[3px] bg-muted/30 p-1 rounded-lg">
|
||||
{days.map((day, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={cn("flex-1 h-6 rounded-[5px] transition-colors", day.completed ? "bg-green-600" : "bg-red-500/60")}
|
||||
title={day.date ? day.date.toLocaleDateString() : `Day ${index + 1}`}
|
||||
/>
|
||||
<TooltipProvider delayDuration={50} key={index}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div
|
||||
className={cn(
|
||||
"relative flex-1 h-7 rounded-[4px] transition-all duration-200 cursor-help",
|
||||
"before:absolute before:inset-0 before:rounded-[4px] before:opacity-0 hover:before:opacity-100 before:bg-white/10 before:transition-opacity",
|
||||
"after:absolute after:inset-0 after:rounded-[4px] after:shadow-[inset_0_1px_theme(colors.white/10%)]",
|
||||
day.completed
|
||||
? "bg-gradient-to-b from-green-500/90 to-green-600 shadow-[0_1px_2px_theme(colors.green.600/30%)]"
|
||||
: "bg-gradient-to-b from-red-500/80 to-red-600/90 shadow-[0_1px_2px_theme(colors.red.600/30%)]",
|
||||
)}
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="p-0 overflow-hidden">
|
||||
<div className="px-3 py-2 bg-popover">
|
||||
<p className="font-medium text-sm mb-2">{day.date?.toLocaleDateString()}</p>
|
||||
<div className="space-y-1.5">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<span className="text-xs text-muted-foreground">{t("serviceTracker.uptime")}:</span>
|
||||
<span className={cn("text-xs font-medium", day.uptime > 95 ? "text-green-500" : "text-red-500")}>{day.uptime.toFixed(1)}%</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<span className="text-xs text-muted-foreground">{t("serviceTracker.delay")}:</span>
|
||||
<span
|
||||
className={cn(
|
||||
"text-xs font-medium",
|
||||
day.delay < 100 ? "text-green-500" : day.delay < 300 ? "text-yellow-500" : "text-red-500",
|
||||
)}
|
||||
>
|
||||
{day.delay.toFixed(0)}ms
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
))}
|
||||
</div>
|
||||
|
||||
|
39
src/components/ThemeColorManager.tsx
Normal file
39
src/components/ThemeColorManager.tsx
Normal file
@ -0,0 +1,39 @@
|
||||
"use client"
|
||||
|
||||
import { useTheme } from "@/hooks/use-theme"
|
||||
import { useEffect } from "react"
|
||||
|
||||
export function ThemeColorManager() {
|
||||
const { theme } = useTheme()
|
||||
|
||||
useEffect(() => {
|
||||
const updateThemeColor = () => {
|
||||
const currentTheme = theme
|
||||
const meta = document.querySelector('meta[name="theme-color"]')
|
||||
|
||||
if (!meta) {
|
||||
const newMeta = document.createElement("meta")
|
||||
newMeta.name = "theme-color"
|
||||
document.head.appendChild(newMeta)
|
||||
}
|
||||
|
||||
const themeColor =
|
||||
currentTheme === "dark"
|
||||
? "hsl(30 15% 8%)" // 深色模式背景色
|
||||
: "hsl(0 0% 98%)" // 浅色模式背景色
|
||||
|
||||
document.querySelector('meta[name="theme-color"]')?.setAttribute("content", themeColor)
|
||||
}
|
||||
|
||||
// Update on mount and theme change
|
||||
updateThemeColor()
|
||||
|
||||
// Listen for system theme changes
|
||||
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)")
|
||||
mediaQuery.addEventListener("change", updateThemeColor)
|
||||
|
||||
return () => mediaQuery.removeEventListener("change", updateThemeColor)
|
||||
}, [theme])
|
||||
|
||||
return null
|
||||
}
|
126
src/components/ui/select.tsx
Normal file
126
src/components/ui/select.tsx
Normal file
@ -0,0 +1,126 @@
|
||||
import { cn } from "@/lib/utils"
|
||||
import * as SelectPrimitive from "@radix-ui/react-select"
|
||||
import { Check, ChevronDown, ChevronUp } from "lucide-react"
|
||||
import * as React from "react"
|
||||
|
||||
const Select = SelectPrimitive.Root
|
||||
|
||||
const SelectGroup = SelectPrimitive.Group
|
||||
|
||||
const SelectValue = SelectPrimitive.Value
|
||||
|
||||
const SelectTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<SelectPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<SelectPrimitive.Icon asChild>
|
||||
<ChevronDown className="h-4 w-4 opacity-50" />
|
||||
</SelectPrimitive.Icon>
|
||||
</SelectPrimitive.Trigger>
|
||||
))
|
||||
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
|
||||
|
||||
const SelectScrollUpButton = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.ScrollUpButton ref={ref} className={cn("flex cursor-default items-center justify-center py-1", className)} {...props}>
|
||||
<ChevronUp className="h-4 w-4" />
|
||||
</SelectPrimitive.ScrollUpButton>
|
||||
))
|
||||
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
|
||||
|
||||
const SelectScrollDownButton = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.ScrollDownButton ref={ref} className={cn("flex cursor-default items-center justify-center py-1", className)} {...props}>
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
</SelectPrimitive.ScrollDownButton>
|
||||
))
|
||||
SelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName
|
||||
|
||||
const SelectContent = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
|
||||
>(({ className, children, position = "popper", ...props }, ref) => (
|
||||
<SelectPrimitive.Portal>
|
||||
<SelectPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
position === "popper" &&
|
||||
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||
className,
|
||||
)}
|
||||
position={position}
|
||||
{...props}
|
||||
>
|
||||
<SelectScrollUpButton />
|
||||
<SelectPrimitive.Viewport
|
||||
className={cn("p-1", position === "popper" && "h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]")}
|
||||
>
|
||||
{children}
|
||||
</SelectPrimitive.Viewport>
|
||||
<SelectScrollDownButton />
|
||||
</SelectPrimitive.Content>
|
||||
</SelectPrimitive.Portal>
|
||||
))
|
||||
SelectContent.displayName = SelectPrimitive.Content.displayName
|
||||
|
||||
const SelectLabel = React.forwardRef<React.ElementRef<typeof SelectPrimitive.Label>, React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.Label ref={ref} className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)} {...props} />
|
||||
),
|
||||
)
|
||||
SelectLabel.displayName = SelectPrimitive.Label.displayName
|
||||
|
||||
const SelectItem = React.forwardRef<React.ElementRef<typeof SelectPrimitive.Item>, React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>>(
|
||||
({ className, children, ...props }, ref) => (
|
||||
<SelectPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<SelectPrimitive.ItemIndicator>
|
||||
<Check className="h-4 w-4" />
|
||||
</SelectPrimitive.ItemIndicator>
|
||||
</span>
|
||||
|
||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||
</SelectPrimitive.Item>
|
||||
),
|
||||
)
|
||||
SelectItem.displayName = SelectPrimitive.Item.displayName
|
||||
|
||||
const SelectSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => <SelectPrimitive.Separator ref={ref} className={cn("-mx-1 my-1 h-px bg-muted", className)} {...props} />)
|
||||
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
|
||||
|
||||
export {
|
||||
Select,
|
||||
SelectGroup,
|
||||
SelectValue,
|
||||
SelectTrigger,
|
||||
SelectContent,
|
||||
SelectLabel,
|
||||
SelectItem,
|
||||
SelectSeparator,
|
||||
SelectScrollUpButton,
|
||||
SelectScrollDownButton,
|
||||
}
|
27
src/components/ui/tooltip.tsx
Normal file
27
src/components/ui/tooltip.tsx
Normal file
@ -0,0 +1,27 @@
|
||||
import { cn } from "@/lib/utils"
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
|
||||
import * as React from "react"
|
||||
|
||||
const TooltipProvider = TooltipPrimitive.Provider
|
||||
|
||||
const Tooltip = TooltipPrimitive.Root
|
||||
|
||||
const TooltipTrigger = TooltipPrimitive.Trigger
|
||||
|
||||
const TooltipContent = React.forwardRef<
|
||||
React.ElementRef<typeof TooltipPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
|
||||
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||
<TooltipPrimitive.Content
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 overflow-hidden rounded-[8px] border font-medium bg-popover px-1.5 py-0.5 text-xs text-popover-foreground shadow-lg animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TooltipContent.displayName = TooltipPrimitive.Content.displayName
|
||||
|
||||
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
|
@ -4,6 +4,10 @@ declare global {
|
||||
interface Window {
|
||||
CustomBackgroundImage: string
|
||||
CustomMobileBackgroundImage: string
|
||||
ForceShowServices: boolean
|
||||
ForceCardInline: boolean
|
||||
ForceShowMap: boolean
|
||||
ForcePeakCutEnabled: boolean
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -72,6 +72,8 @@ export function getDaysBetweenDatesWithAutoRenewal({ autoRenewal, cycle, startDa
|
||||
months = 12
|
||||
break
|
||||
case "季":
|
||||
case "q":
|
||||
case "qr":
|
||||
case "quarterly":
|
||||
cycleLabel = "季"
|
||||
months = 3
|
||||
|
@ -46,6 +46,7 @@
|
||||
"serviceTracker": {
|
||||
"noService": "No service data",
|
||||
"uptime": "Uptime",
|
||||
"delay": "Delay",
|
||||
"daysAgo": "days ago",
|
||||
"today": "Today",
|
||||
"loading": "Loading..."
|
||||
|
@ -44,8 +44,9 @@
|
||||
"nextUpdate": "下次更新"
|
||||
},
|
||||
"serviceTracker": {
|
||||
"noService": "没有服务监测数据",
|
||||
"uptime": "可用率",
|
||||
"noService": "无服务数据",
|
||||
"uptime": "在线率",
|
||||
"delay": "延迟",
|
||||
"daysAgo": "天前",
|
||||
"today": "今天",
|
||||
"loading": "加载中..."
|
||||
|
@ -44,8 +44,9 @@
|
||||
"nextUpdate": "下次更新"
|
||||
},
|
||||
"serviceTracker": {
|
||||
"noService": "沒有服務監控數據",
|
||||
"uptime": "可用率",
|
||||
"noService": "無服務數據",
|
||||
"uptime": "在線率",
|
||||
"delay": "延遲",
|
||||
"daysAgo": "天前",
|
||||
"today": "今天",
|
||||
"loading": "載入中..."
|
||||
|
@ -4,6 +4,7 @@ import ReactDOM from "react-dom/client"
|
||||
import { Toaster } from "sonner"
|
||||
|
||||
import App from "./App"
|
||||
import { ThemeColorManager } from "./components/ThemeColorManager"
|
||||
import { ThemeProvider } from "./components/ThemeProvider"
|
||||
import { MotionProvider } from "./components/motion/motion-provider"
|
||||
import { SortProvider } from "./context/sort-provider"
|
||||
@ -18,6 +19,7 @@ const queryClient = new QueryClient()
|
||||
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||
<MotionProvider>
|
||||
<ThemeProvider storageKey="vite-ui-theme">
|
||||
<ThemeColorManager />
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<WebSocketProvider url="/api/v1/ws/server">
|
||||
<StatusProvider>
|
||||
|
@ -7,6 +7,7 @@ import { ServiceTracker } from "@/components/ServiceTracker"
|
||||
import { Loader } from "@/components/loading/Loader"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||
import { SORT_ORDERS, SORT_TYPES } from "@/context/sort-context"
|
||||
import { useSort } from "@/hooks/use-sort"
|
||||
import { useStatus } from "@/hooks/use-status"
|
||||
@ -53,18 +54,31 @@ export default function Servers() {
|
||||
|
||||
useEffect(() => {
|
||||
const showServicesState = localStorage.getItem("showServices")
|
||||
if (showServicesState !== null) {
|
||||
if (window.ForceShowServices) {
|
||||
setShowServices("1")
|
||||
} else if (showServicesState !== null) {
|
||||
setShowServices(showServicesState)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const inlineState = localStorage.getItem("inline")
|
||||
if (inlineState !== null) {
|
||||
if (window.ForceCardInline) {
|
||||
setInline("1")
|
||||
} else if (inlineState !== null) {
|
||||
setInline(inlineState)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const showMapState = localStorage.getItem("showMap")
|
||||
if (window.ForceShowMap) {
|
||||
setShowMap("1")
|
||||
} else if (showMapState !== null) {
|
||||
setShowMap(showMapState)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const savedGroup = sessionStorage.getItem("selectedGroup") || "All"
|
||||
setCurrentGroup(savedGroup)
|
||||
@ -212,18 +226,24 @@ export default function Servers() {
|
||||
<button
|
||||
onClick={() => {
|
||||
setShowMap(showMap === "0" ? "1" : "0")
|
||||
localStorage.setItem("showMap", showMap === "0" ? "1" : "0")
|
||||
}}
|
||||
className={cn(
|
||||
"rounded-[50px] text-white cursor-pointer [text-shadow:_0_1px_0_rgb(0_0_0_/_20%)] bg-blue-600 p-[10px] transition-all shadow-[inset_0_1px_0_rgba(255,255,255,0.2)] ",
|
||||
"rounded-[50px] bg-white dark:bg-stone-800 cursor-pointer p-[10px] transition-all border dark:border-none border-stone-200 dark:border-stone-700 hover:bg-stone-100 dark:hover:bg-stone-700 shadow-[inset_0_1px_0_rgba(255,255,255,0.2)]",
|
||||
{
|
||||
"shadow-[inset_0_1px_0_rgba(0,0,0,0.2)] bg-blue-500": showMap === "1",
|
||||
"shadow-[inset_0_1px_0_rgba(0,0,0,0.2)] !bg-blue-600 hover:!bg-blue-600 border-blue-600 dark:border-blue-600": showMap === "1",
|
||||
"text-white": showMap === "1",
|
||||
},
|
||||
{
|
||||
"bg-opacity-70": customBackgroundImage,
|
||||
"bg-opacity-70 dark:bg-opacity-70": customBackgroundImage,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<MapIcon className="size-[13px]" />
|
||||
<MapIcon
|
||||
className={cn("size-[13px]", {
|
||||
"text-white": showMap === "1",
|
||||
})}
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
@ -231,16 +251,21 @@ export default function Servers() {
|
||||
localStorage.setItem("showServices", showServices === "0" ? "1" : "0")
|
||||
}}
|
||||
className={cn(
|
||||
"rounded-[50px] text-white cursor-pointer [text-shadow:_0_1px_0_rgb(0_0_0_/_20%)] bg-blue-600 p-[10px] transition-all shadow-[inset_0_1px_0_rgba(255,255,255,0.2)] ",
|
||||
"rounded-[50px] bg-white dark:bg-stone-800 cursor-pointer p-[10px] transition-all border dark:border-none border-stone-200 dark:border-stone-700 hover:bg-stone-100 dark:hover:bg-stone-700 shadow-[inset_0_1px_0_rgba(255,255,255,0.2)]",
|
||||
{
|
||||
"shadow-[inset_0_1px_0_rgba(0,0,0,0.2)] bg-blue-500": showServices === "1",
|
||||
"shadow-[inset_0_1px_0_rgba(0,0,0,0.2)] !bg-blue-600 hover:!bg-blue-600 border-blue-600 dark:border-blue-600": showServices === "1",
|
||||
"text-white": showServices === "1",
|
||||
},
|
||||
{
|
||||
"bg-opacity-70": customBackgroundImage,
|
||||
"bg-opacity-70 dark:bg-opacity-70": customBackgroundImage,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<ChartBarSquareIcon className="size-[13px]" />
|
||||
<ChartBarSquareIcon
|
||||
className={cn("size-[13px]", {
|
||||
"text-white": showServices === "1",
|
||||
})}
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
@ -248,16 +273,21 @@ export default function Servers() {
|
||||
localStorage.setItem("inline", inline === "0" ? "1" : "0")
|
||||
}}
|
||||
className={cn(
|
||||
"rounded-[50px] text-white cursor-pointer [text-shadow:_0_1px_0_rgb(0_0_0_/_20%)] bg-blue-600 p-[10px] transition-all shadow-[inset_0_1px_0_rgba(255,255,255,0.2)] ",
|
||||
"rounded-[50px] bg-white dark:bg-stone-800 cursor-pointer p-[10px] transition-all border dark:border-none border-stone-200 dark:border-stone-700 hover:bg-stone-100 dark:hover:bg-stone-700 shadow-[inset_0_1px_0_rgba(255,255,255,0.2)]",
|
||||
{
|
||||
"shadow-[inset_0_1px_0_rgba(0,0,0,0.2)] bg-blue-500": inline === "1",
|
||||
"shadow-[inset_0_1px_0_rgba(0,0,0,0.2)] !bg-blue-600 hover:!bg-blue-600 border-blue-600 dark:border-blue-600": inline === "1",
|
||||
"text-white": inline === "1",
|
||||
},
|
||||
{
|
||||
"bg-opacity-70": customBackgroundImage,
|
||||
"bg-opacity-70 dark:bg-opacity-70": customBackgroundImage,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<ViewColumnsIcon className="size-[13px]" />
|
||||
<ViewColumnsIcon
|
||||
className={cn("size-[13px]", {
|
||||
"text-white": inline === "1",
|
||||
})}
|
||||
/>
|
||||
</button>
|
||||
<GroupSwitch tabs={groupTabs} currentTab={currentGroup} setCurrentTab={handleTagChange} />
|
||||
</section>
|
||||
@ -265,7 +295,7 @@ export default function Servers() {
|
||||
<PopoverTrigger asChild>
|
||||
<button
|
||||
className={cn(
|
||||
"rounded-[50px] flex items-center gap-1 dark:text-white border dark:border-none text-black cursor-pointer dark:[text-shadow:_0_1px_0_rgb(0_0_0_/_20%)] dark:bg-stone-800 bg-stone-100 p-[10px] transition-all shadow-[inset_0_1px_0_rgba(255,255,255,0.2)] ",
|
||||
"rounded-[50px] flex items-center gap-1 dark:text-white border dark:border-none text-black cursor-pointer dark:[text-shadow:_0_1px_0_rgb(0_0_0_/_20%)] dark:bg-stone-800 bg-white p-[10px] transition-all shadow-[inset_0_1px_0_rgba(255,255,255,0.2)] ",
|
||||
{
|
||||
"shadow-[inset_0_1px_0_rgba(0,0,0,0.2)] dark:bg-stone-700 bg-stone-200": settingsOpen,
|
||||
},
|
||||
@ -284,47 +314,38 @@ export default function Servers() {
|
||||
)}
|
||||
</button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="py-2 px-2 w-fit max-w-60 rounded-[8px]">
|
||||
<div className="flex flex-col gap-2">
|
||||
<section className="flex flex-col gap-1">
|
||||
<Label className=" text-stone-500 text-xs">Sort by</Label>
|
||||
<section className="flex items-center gap-1 flex-wrap">
|
||||
{SORT_TYPES.map((type) => (
|
||||
<button
|
||||
key={type}
|
||||
onClick={() => setSortType(type)}
|
||||
className={cn(
|
||||
"rounded-[5px] text-[11px] w-fit px-1 py-0.5 cursor-pointer bg-transparent border transition-all dark:shadow-none ",
|
||||
{
|
||||
"bg-black text-white dark:bg-white dark:text-black shadow-[inset_0_1px_0_rgba(255,255,255,0.2)]": sortType === type,
|
||||
},
|
||||
)}
|
||||
>
|
||||
{type}
|
||||
</button>
|
||||
))}
|
||||
</section>
|
||||
</section>
|
||||
<section className="flex flex-col gap-1">
|
||||
<Label className=" text-stone-500 text-xs">Sort order</Label>
|
||||
<section className="flex items-center gap-1">
|
||||
{SORT_ORDERS.map((order) => (
|
||||
<button
|
||||
disabled={sortType === "default"}
|
||||
key={order}
|
||||
onClick={() => setSortOrder(order)}
|
||||
className={cn(
|
||||
"rounded-[5px] text-[11px] w-fit px-1 py-0.5 cursor-pointer bg-transparent border transition-all shadow-[inset_0_1px_0_rgba(255,255,255,0.2)] dark:shadow-none",
|
||||
{
|
||||
"bg-black text-white dark:bg-white dark:text-black": sortOrder === order && sortType !== "default",
|
||||
},
|
||||
)}
|
||||
>
|
||||
{order}
|
||||
</button>
|
||||
))}
|
||||
</section>
|
||||
</section>
|
||||
<PopoverContent className="p-4 w-[240px] rounded-lg">
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs font-medium text-muted-foreground">Sort by</Label>
|
||||
<Select value={sortType} onValueChange={setSortType}>
|
||||
<SelectTrigger className="w-full text-xs h-8">
|
||||
<SelectValue placeholder="Choose type" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{SORT_TYPES.map((type) => (
|
||||
<SelectItem key={type} value={type} className="text-xs">
|
||||
{type.charAt(0).toUpperCase() + type.slice(1)}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs font-medium text-muted-foreground">Sort order</Label>
|
||||
<Select value={sortOrder} onValueChange={setSortOrder} disabled={sortType === "default"}>
|
||||
<SelectTrigger className="w-full text-xs h-8">
|
||||
<SelectValue placeholder="Choose order" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{SORT_ORDERS.map((order) => (
|
||||
<SelectItem key={order} value={order} className="text-xs">
|
||||
{order.charAt(0).toUpperCase() + order.slice(1)}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
@ -1,5 +1,6 @@
|
||||
import react from "@vitejs/plugin-react-swc"
|
||||
import { execSync } from "child_process"
|
||||
import fs from "fs"
|
||||
import path from "path"
|
||||
import { defineConfig } from "vite"
|
||||
|
||||
@ -26,6 +27,10 @@ export default defineConfig({
|
||||
},
|
||||
},
|
||||
server: {
|
||||
https: {
|
||||
key: fs.readFileSync("./.cert/key.pem"),
|
||||
cert: fs.readFileSync("./.cert/cert.pem"),
|
||||
},
|
||||
proxy: {
|
||||
"/api/v1/ws/server": {
|
||||
target: "ws://localhost:18009",
|
||||
|
Loading…
x
Reference in New Issue
Block a user