feat: refactor refresh

This commit is contained in:
hamster1963 2024-12-26 11:07:52 +08:00
parent 704818d084
commit 5334fd2acb
10 changed files with 93 additions and 28 deletions

View File

@ -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 />} />

View File

@ -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])

View File

@ -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>
)
}

View File

@ -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: () => {},
}) })

View File

@ -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>

View File

@ -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",

View File

@ -6,6 +6,7 @@
"online": "在线", "online": "在线",
"offline": "离线", "offline": "离线",
"whereTheTimeIs": "当前时间", "whereTheTimeIs": "当前时间",
"refreshing": "刷新中",
"info": { "info": {
"websocketConnecting": "WebSocket 连接中", "websocketConnecting": "WebSocket 连接中",
"websocketConnected": "WebSocket 连接成功", "websocketConnected": "WebSocket 连接成功",

View File

@ -6,6 +6,7 @@
"online": "在線", "online": "在線",
"offline": "離線", "offline": "離線",
"whereTheTimeIs": "目前時間", "whereTheTimeIs": "目前時間",
"refreshing": "刷新中",
"info": { "info": {
"websocketConnecting": "WebSocket 連接中", "websocketConnecting": "WebSocket 連接中",
"websocketConnected": "WebSocket 連接成功", "websocketConnected": "WebSocket 連接成功",

View File

@ -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 ">