=
const progress = (transfer / max) * 100
return (
-
-
- {name}
-
- {new Date(from).toLocaleDateString()} - {new Date(to).toLocaleDateString()}
-
-
-
-
-
-
- {serverName}
-
-
-
{progress.toFixed(0)}%
-
-
-
-
-
-
+
+ {/* Header */}
+
-
-
- {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 (
-
+
+
+
+ ⌘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
>
-
-
{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_TYPES.map((type) => (
-
- ))}
-
-
-
-
-
- {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",