mirror of
https://github.com/woodchen-ink/nezha-dash-v1.git
synced 2025-07-18 17:41:56 +08:00
feat: refactor refresh
This commit is contained in:
parent
704818d084
commit
5334fd2acb
@ -5,7 +5,7 @@ import { Route, BrowserRouter as Router, Routes } from "react-router-dom"
|
|||||||
|
|
||||||
import ErrorBoundary from "./components/ErrorBoundary"
|
import ErrorBoundary from "./components/ErrorBoundary"
|
||||||
import Footer from "./components/Footer"
|
import Footer from "./components/Footer"
|
||||||
import Header from "./components/Header"
|
import Header, { RefreshToast } from "./components/Header"
|
||||||
import { useTheme } from "./hooks/use-theme"
|
import { useTheme } from "./hooks/use-theme"
|
||||||
import { InjectContext } from "./lib/inject"
|
import { InjectContext } from "./lib/inject"
|
||||||
import { fetchSetting } from "./lib/nezha-api"
|
import { fetchSetting } from "./lib/nezha-api"
|
||||||
@ -93,6 +93,7 @@ const App: React.FC = () => {
|
|||||||
>
|
>
|
||||||
<main className="flex z-20 min-h-[calc(100vh-calc(var(--spacing)*16))] flex-1 flex-col gap-4 p-4 md:p-10 md:pt-8">
|
<main className="flex z-20 min-h-[calc(100vh-calc(var(--spacing)*16))] flex-1 flex-col gap-4 p-4 md:p-10 md:pt-8">
|
||||||
<Header />
|
<Header />
|
||||||
|
<RefreshToast />
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<Server />} />
|
<Route path="/" element={<Server />} />
|
||||||
<Route path="/server/:id" element={<ServerDetail />} />
|
<Route path="/server/:id" element={<ServerDetail />} />
|
||||||
|
@ -5,13 +5,14 @@ import { useWebSocketContext } from "@/hooks/use-websocket-context"
|
|||||||
import { fetchLoginUser, fetchSetting } from "@/lib/nezha-api"
|
import { fetchLoginUser, fetchSetting } from "@/lib/nezha-api"
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
import { useQuery } from "@tanstack/react-query"
|
import { useQuery } from "@tanstack/react-query"
|
||||||
|
import { AnimatePresence, m } from "framer-motion"
|
||||||
import { DateTime } from "luxon"
|
import { DateTime } from "luxon"
|
||||||
import { useEffect, useRef, useState } from "react"
|
import { useEffect, useRef, 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"
|
||||||
|
|
||||||
import { LanguageSwitcher } from "./LanguageSwitcher"
|
import { LanguageSwitcher } from "./LanguageSwitcher"
|
||||||
import { Loader } from "./loading/Loader"
|
import { Loader, LoadingSpinner } from "./loading/Loader"
|
||||||
import { Button } from "./ui/button"
|
import { Button } from "./ui/button"
|
||||||
|
|
||||||
function Header() {
|
function Header() {
|
||||||
@ -138,32 +139,75 @@ function Links() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function RefreshToast() {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const navigate = useNavigate()
|
||||||
|
|
||||||
|
const { needReconnect } = useWebSocketContext()
|
||||||
|
|
||||||
|
if (!needReconnect) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (needReconnect) {
|
||||||
|
sessionStorage.removeItem("needRefresh")
|
||||||
|
setTimeout(() => {
|
||||||
|
navigate(0)
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center justify-center">
|
||||||
|
<AnimatePresence>
|
||||||
|
<m.div
|
||||||
|
initial={{ opacity: 0, filter: "blur(10px)", scale: 0.8 }}
|
||||||
|
animate={{ opacity: 1, filter: "blur(0px)", scale: 1 }}
|
||||||
|
exit={{ opacity: 0, filter: "blur(10px)", scale: 0.8 }}
|
||||||
|
transition={{ type: "spring", duration: 0.8 }}
|
||||||
|
className="fixed overflow-hidden top-[35px] z-[999] flex items-center justify-between gap-4 rounded-[50px] border-[1px] border-solid bg-white px-2 py-1.5 shadow-xl shadow-black/5 dark:border-stone-700 dark:bg-stone-800 dark:shadow-none"
|
||||||
|
>
|
||||||
|
<section className="flex items-center gap-1.5">
|
||||||
|
<LoadingSpinner />
|
||||||
|
<p className="text-[12.5px] font-medium">{t("refreshing")}...</p>
|
||||||
|
</section>
|
||||||
|
</m.div>
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function DashboardLink() {
|
function DashboardLink() {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { reconnect } = useWebSocketContext()
|
const { setNeedReconnect } = useWebSocketContext()
|
||||||
const initRef = useRef(false)
|
const previousLoginState = useRef<boolean | null>(null)
|
||||||
const { data: userData } = useQuery({
|
const {
|
||||||
|
data: userData,
|
||||||
|
isFetched,
|
||||||
|
isLoadingError,
|
||||||
|
isError,
|
||||||
|
} = useQuery({
|
||||||
queryKey: ["login-user"],
|
queryKey: ["login-user"],
|
||||||
queryFn: () => fetchLoginUser(),
|
queryFn: () => fetchLoginUser(),
|
||||||
refetchOnMount: true,
|
refetchOnMount: false,
|
||||||
refetchOnWindowFocus: true,
|
refetchOnWindowFocus: true,
|
||||||
refetchIntervalInBackground: true,
|
refetchIntervalInBackground: true,
|
||||||
refetchInterval: 1000 * 60 * 1,
|
refetchInterval: 1000 * 5,
|
||||||
retry: false,
|
retry: 0,
|
||||||
})
|
})
|
||||||
|
|
||||||
let isLogin = !!userData?.data?.id
|
const isLogin = isError ? false : userData ? !!userData?.data?.id && !!document.cookie : false
|
||||||
|
|
||||||
if (!document.cookie) {
|
if (isLoadingError) {
|
||||||
isLogin = false
|
previousLoginState.current = isLogin
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// 当登录状态变化时重新连接 WebSocket
|
if (isFetched || isError) {
|
||||||
if (initRef.current) {
|
// 只有当登录状态发生变化时才设置needReconnect
|
||||||
reconnect()
|
if (previousLoginState.current !== null && previousLoginState.current !== isLogin) {
|
||||||
} else {
|
setNeedReconnect(true)
|
||||||
initRef.current = true
|
}
|
||||||
|
previousLoginState.current = isLogin
|
||||||
}
|
}
|
||||||
}, [isLogin])
|
}, [isLogin])
|
||||||
|
|
||||||
|
@ -11,3 +11,22 @@ export const Loader = ({ visible }: { visible: boolean }) => {
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const LoadingSpinner = () => {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
className={"size-4 animate-spin"}
|
||||||
|
>
|
||||||
|
<path d="M21 12a9 9 0 1 1-6.219-8.56" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
@ -5,6 +5,8 @@ export interface WebSocketContextType {
|
|||||||
connected: boolean
|
connected: boolean
|
||||||
messageHistory: { data: string }[]
|
messageHistory: { data: string }[]
|
||||||
reconnect: () => void
|
reconnect: () => void
|
||||||
|
needReconnect: boolean
|
||||||
|
setNeedReconnect: (needReconnect: boolean) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const WebSocketContext = createContext<WebSocketContextType>({
|
export const WebSocketContext = createContext<WebSocketContextType>({
|
||||||
@ -12,4 +14,6 @@ export const WebSocketContext = createContext<WebSocketContextType>({
|
|||||||
connected: false,
|
connected: false,
|
||||||
messageHistory: [],
|
messageHistory: [],
|
||||||
reconnect: () => {},
|
reconnect: () => {},
|
||||||
|
needReconnect: false,
|
||||||
|
setNeedReconnect: () => {},
|
||||||
})
|
})
|
||||||
|
@ -11,6 +11,7 @@ export const WebSocketProvider: React.FC<WebSocketProviderProps> = ({ url, child
|
|||||||
const [lastMessage, setLastMessage] = useState<{ data: string } | null>(null)
|
const [lastMessage, setLastMessage] = useState<{ data: string } | null>(null)
|
||||||
const [messageHistory, setMessageHistory] = useState<{ data: string }[]>([]) // 新增历史消息状态
|
const [messageHistory, setMessageHistory] = useState<{ data: string }[]>([]) // 新增历史消息状态
|
||||||
const [connected, setConnected] = useState(false)
|
const [connected, setConnected] = useState(false)
|
||||||
|
const [needReconnect, setNeedReconnect] = useState(false)
|
||||||
const ws = useRef<WebSocket | null>(null)
|
const ws = useRef<WebSocket | null>(null)
|
||||||
const reconnectTimeout = useRef<NodeJS.Timeout>(null)
|
const reconnectTimeout = useRef<NodeJS.Timeout>(null)
|
||||||
const maxReconnectAttempts = 30
|
const maxReconnectAttempts = 30
|
||||||
@ -99,7 +100,7 @@ export const WebSocketProvider: React.FC<WebSocketProviderProps> = ({ url, child
|
|||||||
cleanup()
|
cleanup()
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
connect()
|
connect()
|
||||||
}, 100)
|
}, 1000)
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -113,8 +114,10 @@ export const WebSocketProvider: React.FC<WebSocketProviderProps> = ({ url, child
|
|||||||
const contextValue: WebSocketContextType = {
|
const contextValue: WebSocketContextType = {
|
||||||
lastMessage,
|
lastMessage,
|
||||||
connected,
|
connected,
|
||||||
messageHistory, // 添加到 context value
|
messageHistory,
|
||||||
reconnect, // 添加到 context value
|
reconnect,
|
||||||
|
needReconnect,
|
||||||
|
setNeedReconnect,
|
||||||
}
|
}
|
||||||
|
|
||||||
return <WebSocketContext.Provider value={contextValue}>{children}</WebSocketContext.Provider>
|
return <WebSocketContext.Provider value={contextValue}>{children}</WebSocketContext.Provider>
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
"online": "Online",
|
"online": "Online",
|
||||||
"offline": "Offline",
|
"offline": "Offline",
|
||||||
"whereTheTimeIs": "Where the time is",
|
"whereTheTimeIs": "Where the time is",
|
||||||
|
"refreshing": "Refreshing",
|
||||||
"info": {
|
"info": {
|
||||||
"websocketConnecting": "WebSocket connecting",
|
"websocketConnecting": "WebSocket connecting",
|
||||||
"websocketConnected": "WebSocket connected",
|
"websocketConnected": "WebSocket connected",
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
"online": "在线",
|
"online": "在线",
|
||||||
"offline": "离线",
|
"offline": "离线",
|
||||||
"whereTheTimeIs": "当前时间",
|
"whereTheTimeIs": "当前时间",
|
||||||
|
"refreshing": "刷新中",
|
||||||
"info": {
|
"info": {
|
||||||
"websocketConnecting": "WebSocket 连接中",
|
"websocketConnecting": "WebSocket 连接中",
|
||||||
"websocketConnected": "WebSocket 连接成功",
|
"websocketConnected": "WebSocket 连接成功",
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
"online": "在線",
|
"online": "在線",
|
||||||
"offline": "離線",
|
"offline": "離線",
|
||||||
"whereTheTimeIs": "目前時間",
|
"whereTheTimeIs": "目前時間",
|
||||||
|
"refreshing": "刷新中",
|
||||||
"info": {
|
"info": {
|
||||||
"websocketConnecting": "WebSocket 連接中",
|
"websocketConnecting": "WebSocket 連接中",
|
||||||
"websocketConnected": "WebSocket 連接成功",
|
"websocketConnected": "WebSocket 連接成功",
|
||||||
|
@ -19,7 +19,6 @@ import { ArrowDownIcon, ArrowUpIcon, ArrowsUpDownIcon, ChartBarSquareIcon, MapIc
|
|||||||
import { useQuery } from "@tanstack/react-query"
|
import { useQuery } from "@tanstack/react-query"
|
||||||
import { useEffect, useState } from "react"
|
import { useEffect, useState } from "react"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import { toast } from "sonner"
|
|
||||||
|
|
||||||
export default function Servers() {
|
export default function Servers() {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
@ -56,14 +55,6 @@ export default function Servers() {
|
|||||||
|
|
||||||
const groupTabs = ["All", ...(groupData?.data?.map((item: ServerGroup) => item.group.name) || [])]
|
const groupTabs = ["All", ...(groupData?.data?.map((item: ServerGroup) => item.group.name) || [])]
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const hasShownToast = sessionStorage.getItem("websocket-connected-toast")
|
|
||||||
if (connected && !hasShownToast) {
|
|
||||||
toast.success(t("info.websocketConnected"))
|
|
||||||
sessionStorage.setItem("websocket-connected-toast", "true")
|
|
||||||
}
|
|
||||||
}, [connected])
|
|
||||||
|
|
||||||
if (!connected && !lastMessage) {
|
if (!connected && !lastMessage) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center min-h-96 justify-center ">
|
<div className="flex flex-col items-center min-h-96 justify-center ">
|
||||||
|
Loading…
x
Reference in New Issue
Block a user