diff --git a/.cert/cert.pem b/.cert/cert.pem new file mode 100644 index 0000000..22a7231 --- /dev/null +++ b/.cert/cert.pem @@ -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----- diff --git a/.cert/key.pem b/.cert/key.pem new file mode 100644 index 0000000..53b20f4 --- /dev/null +++ b/.cert/key.pem @@ -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----- diff --git a/bun.lockb b/bun.lockb index 1afa12c..d4480dc 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/index.html b/index.html index 0a8d619..2b5a298 100644 --- a/index.html +++ b/index.html @@ -107,9 +107,17 @@ - CZL SVR - - + CZL Server + + + + + + + + + +
diff --git a/package.json b/package.json index e60af1c..2bc3abb 100644 --- a/package.json +++ b/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" } } diff --git a/public/manifest.json b/public/manifest.json new file mode 100644 index 0000000..8c9d7d3 --- /dev/null +++ b/public/manifest.json @@ -0,0 +1,17 @@ +{ + "name": "CZL Server", + "short_name": "CZL Server", + "icons": [ + { + "src": "/apple-touch-icon.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" +} diff --git a/src/components/CycleTransferStats.tsx b/src/components/CycleTransferStats.tsx index 814fafb..a1ad817 100644 --- a/src/components/CycleTransferStats.tsx +++ b/src/components/CycleTransferStats.tsx @@ -17,7 +17,7 @@ export const CycleTransferStatsCard: React.FC = ({ serv const serverIdList = serverList.map((server) => server.id.toString()) return ( -
+
{Object.entries(cycleStats).map(([cycleId, cycleData]) => { if (!cycleData.server_name) { return null diff --git a/src/components/CycleTransferStatsClient.tsx b/src/components/CycleTransferStatsClient.tsx index 85419ca..476186f 100644 --- a/src/components/CycleTransferStatsClient.tsx +++ b/src/components/CycleTransferStatsClient.tsx @@ -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 = return (
= const progress = (transfer / max) * 100 return ( -
-
-
{name}
- - {new Date(from).toLocaleDateString()} - {new Date(to).toLocaleDateString()} - -
- -
-
- - {serverName} -
-
-

{progress.toFixed(0)}%

- -
-
- -
-
+
+ {/* Header */} +
+ {serverName} +
{name}
-
- - {formatBytes(transfer)} {t("cycleTransfer.used")} - - - {formatBytes(max)} {t("cycleTransfer.total")} - -
- -
-
- {t("cycleTransfer.nextUpdate")}: {new Date(nextUpdate).toLocaleString()} + {/* Progress Section */} +
+
+
+ {formatBytes(transfer)} + / {formatBytes(max)} +
+ {progress.toFixed(1)}%
-
+ +
+
+
+
+
+ + {/* Footer */} +
+ + {new Date(from).toLocaleDateString()} - {new Date(to).toLocaleDateString()} + + + {t("cycleTransfer.nextUpdate")}: {new Date(nextUpdate).toLocaleString()} + +
) })} diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx index 5de9cf7..b6b9ec3 100644 --- a/src/components/Footer.tsx +++ b/src/components/Footer.tsx @@ -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 (
@@ -11,9 +21,16 @@ const Footer: React.FC = () => { CZL LTD
-

+

+

+ + K + +

+

All Rights Reserved -

+

+
diff --git a/src/components/Header.tsx b/src/components/Header.tsx index 555be57..9b140b4 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -293,7 +293,7 @@ function Overview() {
- +

:{time.ss.toString().padStart(2, "0")}

diff --git a/src/components/NetworkChart.tsx b/src/components/NetworkChart.tsx index d6fba05..30b09ac 100644 --- a/src/components/NetworkChart.tsx +++ b/src/components/NetworkChart.tsx @@ -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({ 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}` + }} /> `${value}ms`} /> - -
-

{t("serverDetail.region")}

-
-
{countries.getName(country_code?.toUpperCase(), "en")}
- {country_code && } -
-
-
- + + + + + +
+

{t("serverDetail.region")}

+
+
{country_code?.toUpperCase()}
+ {country_code && } +
+
+
+
+
+ +

{countries.getName(country_code?.toUpperCase(), "en")}

+
+
+
)}
diff --git a/src/components/ServiceTracker.tsx b/src/components/ServiceTracker.tsx index 4efbb2f..2d96695 100644 --- a/src/components/ServiceTracker.tsx +++ b/src/components/ServiceTracker.tsx @@ -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) diff --git a/src/components/ServiceTrackerClient.tsx b/src/components/ServiceTrackerClient.tsx index dfc5376..b18afaf 100644 --- a/src/components/ServiceTrackerClient.tsx +++ b/src/components/ServiceTrackerClient.tsx @@ -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 = ({ 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 (
= ({ days, clas >
-
-
-
+
{title}
-
- {avgDelay.toFixed(0)}ms - - +
+ {avgDelay.toFixed(0)}ms + + {uptime.toFixed(1)}% {t("serviceTracker.uptime")}
-
+
{days.map((day, index) => ( -
+ + + +
+ + +
+

{day.date?.toLocaleDateString()}

+
+
+ {t("serviceTracker.uptime")}: + 95 ? "text-green-500" : "text-red-500")}>{day.uptime.toFixed(1)}% +
+
+ {t("serviceTracker.delay")}: + + {day.delay.toFixed(0)}ms + +
+
+
+
+ + ))}
diff --git a/src/components/ThemeColorManager.tsx b/src/components/ThemeColorManager.tsx new file mode 100644 index 0000000..36e2f67 --- /dev/null +++ b/src/components/ThemeColorManager.tsx @@ -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 +} diff --git a/src/components/ui/select.tsx b/src/components/ui/select.tsx new file mode 100644 index 0000000..f0e62b1 --- /dev/null +++ b/src/components/ui/select.tsx @@ -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, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + span]:line-clamp-1", + className, + )} + {...props} + > + {children} + + + + +)) +SelectTrigger.displayName = SelectPrimitive.Trigger.displayName + +const SelectScrollUpButton = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)) +SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName + +const SelectScrollDownButton = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)) +SelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName + +const SelectContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, position = "popper", ...props }, ref) => ( + + + + + {children} + + + + +)) +SelectContent.displayName = SelectPrimitive.Content.displayName + +const SelectLabel = React.forwardRef, React.ComponentPropsWithoutRef>( + ({ className, ...props }, ref) => ( + + ), +) +SelectLabel.displayName = SelectPrimitive.Label.displayName + +const SelectItem = React.forwardRef, React.ComponentPropsWithoutRef>( + ({ className, children, ...props }, ref) => ( + + + + + + + + {children} + + ), +) +SelectItem.displayName = SelectPrimitive.Item.displayName + +const SelectSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ) +SelectSeparator.displayName = SelectPrimitive.Separator.displayName + +export { + Select, + SelectGroup, + SelectValue, + SelectTrigger, + SelectContent, + SelectLabel, + SelectItem, + SelectSeparator, + SelectScrollUpButton, + SelectScrollDownButton, +} diff --git a/src/components/ui/tooltip.tsx b/src/components/ui/tooltip.tsx new file mode 100644 index 0000000..79c7188 --- /dev/null +++ b/src/components/ui/tooltip.tsx @@ -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, + React.ComponentPropsWithoutRef +>(({ className, sideOffset = 4, ...props }, ref) => ( + +)) +TooltipContent.displayName = TooltipPrimitive.Content.displayName + +export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } diff --git a/src/hooks/use-background.ts b/src/hooks/use-background.ts index addcac0..4c642d9 100644 --- a/src/hooks/use-background.ts +++ b/src/hooks/use-background.ts @@ -4,6 +4,10 @@ declare global { interface Window { CustomBackgroundImage: string CustomMobileBackgroundImage: string + ForceShowServices: boolean + ForceCardInline: boolean + ForceShowMap: boolean + ForcePeakCutEnabled: boolean } } diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 16bff0c..59248a7 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -72,6 +72,8 @@ export function getDaysBetweenDatesWithAutoRenewal({ autoRenewal, cycle, startDa months = 12 break case "季": + case "q": + case "qr": case "quarterly": cycleLabel = "季" months = 3 diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index 98811dd..0d42f9c 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -46,6 +46,7 @@ "serviceTracker": { "noService": "No service data", "uptime": "Uptime", + "delay": "Delay", "daysAgo": "days ago", "today": "Today", "loading": "Loading..." diff --git a/src/locales/zh-CN/translation.json b/src/locales/zh-CN/translation.json index 0fdf110..9deebaf 100644 --- a/src/locales/zh-CN/translation.json +++ b/src/locales/zh-CN/translation.json @@ -44,8 +44,9 @@ "nextUpdate": "下次更新" }, "serviceTracker": { - "noService": "没有服务监测数据", - "uptime": "可用率", + "noService": "无服务数据", + "uptime": "在线率", + "delay": "延迟", "daysAgo": "天前", "today": "今天", "loading": "加载中..." diff --git a/src/locales/zh-TW/translation.json b/src/locales/zh-TW/translation.json index 969acb7..9f79200 100644 --- a/src/locales/zh-TW/translation.json +++ b/src/locales/zh-TW/translation.json @@ -44,8 +44,9 @@ "nextUpdate": "下次更新" }, "serviceTracker": { - "noService": "沒有服務監控數據", - "uptime": "可用率", + "noService": "無服務數據", + "uptime": "在線率", + "delay": "延遲", "daysAgo": "天前", "today": "今天", "loading": "載入中..." diff --git a/src/main.tsx b/src/main.tsx index 1be15b4..a8048c8 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -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( + diff --git a/src/pages/Server.tsx b/src/pages/Server.tsx index db37c74..cc77b93 100644 --- a/src/pages/Server.tsx +++ b/src/pages/Server.tsx @@ -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() {
@@ -265,7 +295,7 @@ export default function Servers() { - ))} - - -
- -
- {SORT_ORDERS.map((order) => ( - - ))} -
-
+ +
+
+ + +
+
+ + +
diff --git a/vite.config.ts b/vite.config.ts index d842f91..7e13d99 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -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",