mirror of
https://github.com/woodchen-ink/nezha-dash-v1.git
synced 2025-07-18 17:41:56 +08:00
commit
3d8293cf32
10
package.json
10
package.json
@ -24,10 +24,10 @@
|
|||||||
"@radix-ui/react-separator": "^1.1.1",
|
"@radix-ui/react-separator": "^1.1.1",
|
||||||
"@radix-ui/react-slot": "^1.1.1",
|
"@radix-ui/react-slot": "^1.1.1",
|
||||||
"@radix-ui/react-switch": "^1.1.2",
|
"@radix-ui/react-switch": "^1.1.2",
|
||||||
"@tanstack/react-query": "^5.62.11",
|
"@tanstack/react-query": "^5.62.14",
|
||||||
"@tanstack/react-query-devtools": "^5.62.11",
|
"@tanstack/react-query-devtools": "^5.62.14",
|
||||||
"@tanstack/react-table": "^8.20.6",
|
"@tanstack/react-table": "^8.20.6",
|
||||||
"@trivago/prettier-plugin-sort-imports": "^5.2.0",
|
"@trivago/prettier-plugin-sort-imports": "^5.2.1",
|
||||||
"@types/d3-geo": "^3.1.0",
|
"@types/d3-geo": "^3.1.0",
|
||||||
"@types/luxon": "^3.4.2",
|
"@types/luxon": "^3.4.2",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
@ -51,7 +51,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.17.0",
|
"@eslint/js": "^9.17.0",
|
||||||
"@types/node": "^22.10.3",
|
"@types/node": "^22.10.5",
|
||||||
"@types/react": "^19.0.2",
|
"@types/react": "^19.0.2",
|
||||||
"@types/react-dom": "^19.0.2",
|
"@types/react-dom": "^19.0.2",
|
||||||
"@vitejs/plugin-react-swc": "^3.7.2",
|
"@vitejs/plugin-react-swc": "^3.7.2",
|
||||||
@ -64,6 +64,6 @@
|
|||||||
"tailwindcss": "^3.4.17",
|
"tailwindcss": "^3.4.17",
|
||||||
"typescript": "~5.6.3",
|
"typescript": "~5.6.3",
|
||||||
"typescript-eslint": "^8.19.0",
|
"typescript-eslint": "^8.19.0",
|
||||||
"vite": "^6.0.6"
|
"vite": "^6.0.7"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -37,6 +37,13 @@ export default function GroupSwitch({
|
|||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const savedGroup = sessionStorage.getItem("selectedGroup")
|
||||||
|
if (savedGroup && tabs.includes(savedGroup)) {
|
||||||
|
setCurrentTab(savedGroup)
|
||||||
|
}
|
||||||
|
}, [tabs, setCurrentTab])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const currentTagRef = tagRefs.current[tabs.indexOf(currentTab)]
|
const currentTagRef = tagRefs.current[tabs.indexOf(currentTab)]
|
||||||
|
|
||||||
|
@ -60,7 +60,13 @@ function Header() {
|
|||||||
return (
|
return (
|
||||||
<div className="mx-auto w-full max-w-5xl">
|
<div className="mx-auto w-full max-w-5xl">
|
||||||
<section className="flex items-center justify-between header-top">
|
<section className="flex items-center justify-between header-top">
|
||||||
<section onClick={() => navigate("/")} className="cursor-pointer flex items-center sm:text-base text-sm font-medium">
|
<section
|
||||||
|
onClick={() => {
|
||||||
|
sessionStorage.removeItem("selectedGroup")
|
||||||
|
navigate("/")
|
||||||
|
}}
|
||||||
|
className="cursor-pointer flex items-center sm:text-base text-sm font-medium"
|
||||||
|
>
|
||||||
<div className="mr-1 flex flex-row items-center justify-start header-logo">
|
<div className="mr-1 flex flex-row items-center justify-start header-logo">
|
||||||
<img
|
<img
|
||||||
width={40}
|
width={40}
|
||||||
@ -218,7 +224,6 @@ function DashboardLink() {
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<a
|
<a
|
||||||
href={"/dashboard"}
|
href={"/dashboard"}
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="flex items-center text-nowrap gap-1 text-sm font-medium opacity-50 transition-opacity hover:opacity-100"
|
className="flex items-center text-nowrap gap-1 text-sm font-medium opacity-50 transition-opacity hover:opacity-100"
|
||||||
>
|
>
|
||||||
|
@ -20,6 +20,11 @@ export default function ServerCard({ now, serverInfo }: { now: number; serverInf
|
|||||||
serverInfo,
|
serverInfo,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const cardClick = () => {
|
||||||
|
sessionStorage.setItem("fromMainPage", "true")
|
||||||
|
navigate(`/server/${serverInfo.id}`)
|
||||||
|
}
|
||||||
|
|
||||||
const showFlag = true
|
const showFlag = true
|
||||||
|
|
||||||
const customBackgroundImage =
|
const customBackgroundImage =
|
||||||
@ -46,7 +51,7 @@ export default function ServerCard({ now, serverInfo }: { now: number; serverInf
|
|||||||
"bg-card/70": customBackgroundImage,
|
"bg-card/70": customBackgroundImage,
|
||||||
},
|
},
|
||||||
)}
|
)}
|
||||||
onClick={() => navigate(`/server/${serverInfo.id}`)}
|
onClick={cardClick}
|
||||||
>
|
>
|
||||||
<section
|
<section
|
||||||
className={cn("grid items-center gap-2", {
|
className={cn("grid items-center gap-2", {
|
||||||
@ -157,7 +162,7 @@ export default function ServerCard({ now, serverInfo }: { now: number; serverInf
|
|||||||
"bg-card/70": customBackgroundImage,
|
"bg-card/70": customBackgroundImage,
|
||||||
},
|
},
|
||||||
)}
|
)}
|
||||||
onClick={() => navigate(`/server/${serverInfo.id}`, { replace: true })}
|
onClick={cardClick}
|
||||||
>
|
>
|
||||||
<section
|
<section
|
||||||
className={cn("grid items-center gap-2", {
|
className={cn("grid items-center gap-2", {
|
||||||
|
@ -20,6 +20,11 @@ export default function ServerCardInline({ now, serverInfo }: { now: number; ser
|
|||||||
serverInfo,
|
serverInfo,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const cardClick = () => {
|
||||||
|
sessionStorage.setItem("fromMainPage", "true")
|
||||||
|
navigate(`/server/${serverInfo.id}`)
|
||||||
|
}
|
||||||
|
|
||||||
const showFlag = true
|
const showFlag = true
|
||||||
|
|
||||||
const customBackgroundImage =
|
const customBackgroundImage =
|
||||||
@ -37,7 +42,7 @@ export default function ServerCardInline({ now, serverInfo }: { now: number; ser
|
|||||||
"bg-card/70": customBackgroundImage,
|
"bg-card/70": customBackgroundImage,
|
||||||
},
|
},
|
||||||
)}
|
)}
|
||||||
onClick={() => navigate(`/server/${serverInfo.id}`)}
|
onClick={cardClick}
|
||||||
>
|
>
|
||||||
<section className={cn("grid items-center gap-2 lg:w-36")} style={{ gridTemplateColumns: "auto auto 1fr" }}>
|
<section className={cn("grid items-center gap-2 lg:w-36")} style={{ gridTemplateColumns: "auto auto 1fr" }}>
|
||||||
<span className="h-2 w-2 shrink-0 rounded-full bg-green-500 self-center"></span>
|
<span className="h-2 w-2 shrink-0 rounded-full bg-green-500 self-center"></span>
|
||||||
@ -97,7 +102,7 @@ export default function ServerCardInline({ now, serverInfo }: { now: number; ser
|
|||||||
<div className={"flex w-16 flex-col"}>
|
<div className={"flex w-16 flex-col"}>
|
||||||
<p className="text-xs text-muted-foreground">{t("serverCard.download")}</p>
|
<p className="text-xs text-muted-foreground">{t("serverCard.download")}</p>
|
||||||
<div className="flex items-center text-xs font-semibold">
|
<div className="flex items-center text-xs font-semibold">
|
||||||
{down >= 1024 ? `${(down / 1024).toFixed(2)}G/s` : up >= 1 ? `${down.toFixed(2)}M/s` : `${(down * 1024).toFixed(2)}K/s`}
|
{down >= 1024 ? `${(down / 1024).toFixed(2)}G/s` : down >= 1 ? `${down.toFixed(2)}M/s` : `${(down * 1024).toFixed(2)}K/s`}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={"flex w-20 flex-col"}>
|
<div className={"flex w-20 flex-col"}>
|
||||||
@ -121,7 +126,7 @@ export default function ServerCardInline({ now, serverInfo }: { now: number; ser
|
|||||||
"bg-card/70": customBackgroundImage,
|
"bg-card/70": customBackgroundImage,
|
||||||
},
|
},
|
||||||
)}
|
)}
|
||||||
onClick={() => navigate(`/server/${serverInfo.id}`)}
|
onClick={cardClick}
|
||||||
>
|
>
|
||||||
<section className={cn("grid items-center gap-2 w-40")} style={{ gridTemplateColumns: "auto auto 1fr" }}>
|
<section className={cn("grid items-center gap-2 w-40")} style={{ gridTemplateColumns: "auto auto 1fr" }}>
|
||||||
<span className="h-2 w-2 shrink-0 rounded-full bg-red-500 self-center"></span>
|
<span className="h-2 w-2 shrink-0 rounded-full bg-red-500 self-center"></span>
|
||||||
|
@ -7,6 +7,7 @@ import { useWebSocketContext } from "@/hooks/use-websocket-context"
|
|||||||
import { formatBytes } from "@/lib/format"
|
import { formatBytes } from "@/lib/format"
|
||||||
import { cn, formatNezhaInfo } from "@/lib/utils"
|
import { cn, formatNezhaInfo } from "@/lib/utils"
|
||||||
import { NezhaWebsocketResponse } from "@/types/nezha-api"
|
import { NezhaWebsocketResponse } from "@/types/nezha-api"
|
||||||
|
import { useEffect, useState } from "react"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import { useNavigate } from "react-router-dom"
|
import { useNavigate } from "react-router-dom"
|
||||||
|
|
||||||
@ -16,12 +17,29 @@ export default function ServerDetailOverview({ server_id }: { server_id: string
|
|||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
|
||||||
|
const [hasHistory, setHasHistory] = useState(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const previousPath = sessionStorage.getItem("fromMainPage")
|
||||||
|
if (previousPath) {
|
||||||
|
setHasHistory(true)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
const { lastMessage, connected } = useWebSocketContext()
|
const { lastMessage, connected } = useWebSocketContext()
|
||||||
|
|
||||||
if (!connected && !lastMessage) {
|
if (!connected && !lastMessage) {
|
||||||
return <ServerDetailLoading />
|
return <ServerDetailLoading />
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const linkClick = () => {
|
||||||
|
if (hasHistory) {
|
||||||
|
navigate(-1)
|
||||||
|
} else {
|
||||||
|
navigate("/")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const nezhaWsData = lastMessage ? (JSON.parse(lastMessage.data) as NezhaWebsocketResponse) : null
|
const nezhaWsData = lastMessage ? (JSON.parse(lastMessage.data) as NezhaWebsocketResponse) : null
|
||||||
|
|
||||||
if (!nezhaWsData) {
|
if (!nezhaWsData) {
|
||||||
@ -66,7 +84,7 @@ export default function ServerDetailOverview({ server_id }: { server_id: string
|
|||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
onClick={() => navigate("/")}
|
onClick={linkClick}
|
||||||
className="flex flex-none cursor-pointer font-semibold leading-none items-center break-all tracking-tight gap-1 text-xl server-name"
|
className="flex flex-none cursor-pointer font-semibold leading-none items-center break-all tracking-tight gap-1 text-xl server-name"
|
||||||
>
|
>
|
||||||
<BackIcon />
|
<BackIcon />
|
||||||
|
@ -115,5 +115,6 @@
|
|||||||
},
|
},
|
||||||
"footer": {
|
"footer": {
|
||||||
"themeBy": "Design von "
|
"themeBy": "Design von "
|
||||||
}
|
},
|
||||||
|
"login": "Login"
|
||||||
}
|
}
|
||||||
|
@ -17,7 +17,7 @@ import { NezhaWebsocketResponse } from "@/types/nezha-api"
|
|||||||
import { ServerGroup } from "@/types/nezha-api"
|
import { ServerGroup } from "@/types/nezha-api"
|
||||||
import { ArrowDownIcon, ArrowUpIcon, ArrowsUpDownIcon, ChartBarSquareIcon, MapIcon, ViewColumnsIcon } from "@heroicons/react/20/solid"
|
import { ArrowDownIcon, ArrowUpIcon, ArrowsUpDownIcon, ChartBarSquareIcon, MapIcon, ViewColumnsIcon } from "@heroicons/react/20/solid"
|
||||||
import { useQuery } from "@tanstack/react-query"
|
import { useQuery } from "@tanstack/react-query"
|
||||||
import { useEffect, useState } from "react"
|
import { useEffect, useRef, useState } from "react"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
|
|
||||||
export default function Servers() {
|
export default function Servers() {
|
||||||
@ -32,6 +32,7 @@ export default function Servers() {
|
|||||||
const [showServices, setShowServices] = useState<string>("0")
|
const [showServices, setShowServices] = useState<string>("0")
|
||||||
const [showMap, setShowMap] = useState<string>("0")
|
const [showMap, setShowMap] = useState<string>("0")
|
||||||
const [inline, setInline] = useState<string>("0")
|
const [inline, setInline] = useState<string>("0")
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null)
|
||||||
const [settingsOpen, setSettingsOpen] = useState<boolean>(false)
|
const [settingsOpen, setSettingsOpen] = useState<boolean>(false)
|
||||||
const [currentGroup, setCurrentGroup] = useState<string>("All")
|
const [currentGroup, setCurrentGroup] = useState<string>("All")
|
||||||
|
|
||||||
@ -39,6 +40,19 @@ export default function Servers() {
|
|||||||
// @ts-expect-error CustomBackgroundImage is a global variable
|
// @ts-expect-error CustomBackgroundImage is a global variable
|
||||||
(window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined
|
(window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined
|
||||||
|
|
||||||
|
const restoreScrollPosition = () => {
|
||||||
|
const savedPosition = sessionStorage.getItem("scrollPosition")
|
||||||
|
if (savedPosition && containerRef.current) {
|
||||||
|
containerRef.current.scrollTop = Number(savedPosition)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleTagChange = (newGroup: string) => {
|
||||||
|
setCurrentGroup(newGroup)
|
||||||
|
sessionStorage.setItem("selectedGroup", newGroup)
|
||||||
|
sessionStorage.setItem("scrollPosition", String(containerRef.current?.scrollTop || 0))
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const showServicesState = localStorage.getItem("showServices")
|
const showServicesState = localStorage.getItem("showServices")
|
||||||
if (showServicesState !== null) {
|
if (showServicesState !== null) {
|
||||||
@ -53,6 +67,13 @@ export default function Servers() {
|
|||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const savedGroup = sessionStorage.getItem("selectedGroup") || "All"
|
||||||
|
setCurrentGroup(savedGroup)
|
||||||
|
|
||||||
|
restoreScrollPosition()
|
||||||
|
}, [])
|
||||||
|
|
||||||
const groupTabs = ["All", ...(groupData?.data?.map((item: ServerGroup) => item.group.name) || [])]
|
const groupTabs = ["All", ...(groupData?.data?.map((item: ServerGroup) => item.group.name) || [])]
|
||||||
|
|
||||||
if (!connected && !lastMessage) {
|
if (!connected && !lastMessage) {
|
||||||
@ -233,7 +254,7 @@ export default function Servers() {
|
|||||||
>
|
>
|
||||||
<ViewColumnsIcon className="size-[13px]" />
|
<ViewColumnsIcon className="size-[13px]" />
|
||||||
</button>
|
</button>
|
||||||
<GroupSwitch tabs={groupTabs} currentTab={currentGroup} setCurrentTab={setCurrentGroup} />
|
<GroupSwitch tabs={groupTabs} currentTab={currentGroup} setCurrentTab={handleTagChange} />
|
||||||
</section>
|
</section>
|
||||||
<Popover onOpenChange={setSettingsOpen}>
|
<Popover onOpenChange={setSettingsOpen}>
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
@ -306,14 +327,14 @@ export default function Servers() {
|
|||||||
{showMap === "1" && <GlobalMap now={nezhaWsData.now} serverList={nezhaWsData?.servers || []} />}
|
{showMap === "1" && <GlobalMap now={nezhaWsData.now} serverList={nezhaWsData?.servers || []} />}
|
||||||
{showServices === "1" && <ServiceTracker serverList={filteredServers} />}
|
{showServices === "1" && <ServiceTracker serverList={filteredServers} />}
|
||||||
{inline === "1" && (
|
{inline === "1" && (
|
||||||
<section className="flex flex-col gap-2 overflow-x-scroll scrollbar-hidden mt-6 server-inline-list">
|
<section ref={containerRef} className="flex flex-col gap-2 overflow-x-scroll scrollbar-hidden mt-6 server-inline-list">
|
||||||
{filteredServers.map((serverInfo) => (
|
{filteredServers.map((serverInfo) => (
|
||||||
<ServerCardInline now={nezhaWsData.now} key={serverInfo.id} serverInfo={serverInfo} />
|
<ServerCardInline now={nezhaWsData.now} key={serverInfo.id} serverInfo={serverInfo} />
|
||||||
))}
|
))}
|
||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
{inline === "0" && (
|
{inline === "0" && (
|
||||||
<section className="grid grid-cols-1 gap-2 md:grid-cols-2 mt-6 server-card-list">
|
<section ref={containerRef} className="grid grid-cols-1 gap-2 md:grid-cols-2 mt-6 server-card-list">
|
||||||
{filteredServers.map((serverInfo) => (
|
{filteredServers.map((serverInfo) => (
|
||||||
<ServerCard now={nezhaWsData.now} key={serverInfo.id} serverInfo={serverInfo} />
|
<ServerCard now={nezhaWsData.now} key={serverInfo.id} serverInfo={serverInfo} />
|
||||||
))}
|
))}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user