mirror of
https://github.com/Sagit-chu/flvx.git
synced 2026-10-04 01:06:36 +08:00
refactor(frontend): extract reusable hooks and page helper modules
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
export const getConnectionStatusMeta = (
|
||||
status: "online" | "offline",
|
||||
): { color: "success" | "danger"; text: string } => {
|
||||
if (status === "online") {
|
||||
return { color: "success", text: "在线" };
|
||||
}
|
||||
|
||||
return { color: "danger", text: "离线" };
|
||||
};
|
||||
|
||||
export const getRemoteSyncErrorMessage = (syncError: string): string => {
|
||||
if (syncError === "provider_share_deleted") {
|
||||
return "提供方已删除该分享";
|
||||
}
|
||||
|
||||
if (syncError === "provider_share_disabled") {
|
||||
return "提供方已禁用该分享";
|
||||
}
|
||||
|
||||
if (syncError === "provider_share_expired") {
|
||||
return "提供方分享已过期";
|
||||
}
|
||||
|
||||
return `远程同步失败: ${syncError}`;
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
export const tryCopyInstallCommand = async (
|
||||
command: string,
|
||||
): Promise<boolean> => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(command);
|
||||
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,86 @@
|
||||
export interface NodeSystemInfo {
|
||||
cpuUsage: number;
|
||||
memoryUsage: number;
|
||||
uploadTraffic: number;
|
||||
downloadTraffic: number;
|
||||
uploadSpeed: number;
|
||||
downloadSpeed: number;
|
||||
uptime: number;
|
||||
}
|
||||
|
||||
type RawSystemInfo = Record<string, string | number | undefined>;
|
||||
|
||||
const toInteger = (value: string | number | undefined): number => {
|
||||
return Number.parseInt(String(value ?? 0), 10) || 0;
|
||||
};
|
||||
|
||||
const toFloat = (value: string | number | undefined): number => {
|
||||
return Number.parseFloat(String(value ?? 0)) || 0;
|
||||
};
|
||||
|
||||
const parseRawSystemInfo = (messageData: unknown): RawSystemInfo | null => {
|
||||
if (typeof messageData === "string") {
|
||||
try {
|
||||
const parsed = JSON.parse(messageData);
|
||||
|
||||
if (parsed && typeof parsed === "object") {
|
||||
return parsed as RawSystemInfo;
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (messageData && typeof messageData === "object") {
|
||||
return messageData as RawSystemInfo;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export const buildNodeSystemInfo = (
|
||||
messageData: unknown,
|
||||
previous: NodeSystemInfo | null | undefined,
|
||||
): NodeSystemInfo | null => {
|
||||
const raw = parseRawSystemInfo(messageData);
|
||||
|
||||
if (!raw) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const uploadTraffic = toInteger(raw.bytes_transmitted);
|
||||
const downloadTraffic = toInteger(raw.bytes_received);
|
||||
const uptime = toInteger(raw.uptime);
|
||||
|
||||
let uploadSpeed = 0;
|
||||
let downloadSpeed = 0;
|
||||
|
||||
if (previous && previous.uptime) {
|
||||
const timeDiff = uptime - previous.uptime;
|
||||
|
||||
if (timeDiff > 0 && timeDiff <= 10) {
|
||||
const uploadDiff = uploadTraffic - previous.uploadTraffic;
|
||||
const downloadDiff = downloadTraffic - previous.downloadTraffic;
|
||||
|
||||
if (uploadTraffic >= previous.uploadTraffic && uploadDiff >= 0) {
|
||||
uploadSpeed = uploadDiff / timeDiff;
|
||||
}
|
||||
|
||||
if (downloadTraffic >= previous.downloadTraffic && downloadDiff >= 0) {
|
||||
downloadSpeed = downloadDiff / timeDiff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
cpuUsage: toFloat(raw.cpu_usage),
|
||||
memoryUsage: toFloat(raw.memory_usage),
|
||||
uploadTraffic,
|
||||
downloadTraffic,
|
||||
uploadSpeed,
|
||||
downloadSpeed,
|
||||
uptime,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,61 @@
|
||||
import { useCallback, useEffect, useRef } from "react";
|
||||
|
||||
interface UseNodeOfflineTimersOptions {
|
||||
delayMs?: number;
|
||||
onNodeOffline: (nodeId: number) => void;
|
||||
}
|
||||
|
||||
export const useNodeOfflineTimers = ({
|
||||
delayMs = 3000,
|
||||
onNodeOffline,
|
||||
}: UseNodeOfflineTimersOptions) => {
|
||||
const timersRef = useRef<Map<number, ReturnType<typeof setTimeout>>>(
|
||||
new Map(),
|
||||
);
|
||||
|
||||
const clearOfflineTimer = useCallback((nodeId: number) => {
|
||||
const timer = timersRef.current.get(nodeId);
|
||||
|
||||
if (!timer) {
|
||||
return;
|
||||
}
|
||||
|
||||
clearTimeout(timer);
|
||||
timersRef.current.delete(nodeId);
|
||||
}, []);
|
||||
|
||||
const clearAllOfflineTimers = useCallback(() => {
|
||||
timersRef.current.forEach((timer) => {
|
||||
clearTimeout(timer);
|
||||
});
|
||||
timersRef.current.clear();
|
||||
}, []);
|
||||
|
||||
const scheduleNodeOffline = useCallback(
|
||||
(nodeId: number) => {
|
||||
if (timersRef.current.has(nodeId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
timersRef.current.delete(nodeId);
|
||||
onNodeOffline(nodeId);
|
||||
}, delayMs);
|
||||
|
||||
timersRef.current.set(nodeId, timer);
|
||||
},
|
||||
[delayMs, onNodeOffline],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
clearAllOfflineTimers();
|
||||
};
|
||||
}, [clearAllOfflineTimers]);
|
||||
|
||||
return {
|
||||
clearOfflineTimer,
|
||||
scheduleNodeOffline,
|
||||
clearAllOfflineTimers,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,159 @@
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import axios from "axios";
|
||||
|
||||
import { getToken } from "@/utils/session";
|
||||
|
||||
interface NodeRealtimeMessage {
|
||||
id?: string | number;
|
||||
type?: string;
|
||||
data?: unknown;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
interface UseNodeRealtimeOptions {
|
||||
onMessage: (message: NodeRealtimeMessage) => void;
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
const getRealtimeWsUrl = (): string => {
|
||||
const baseUrl =
|
||||
axios.defaults.baseURL ||
|
||||
(import.meta.env.VITE_API_BASE
|
||||
? `${import.meta.env.VITE_API_BASE}/api/v1/`
|
||||
: "/api/v1/");
|
||||
|
||||
return (
|
||||
baseUrl.replace(/^http/, "ws").replace(/\/api\/v1\/$/, "") +
|
||||
`/system-info?type=0&secret=${getToken() || ""}`
|
||||
);
|
||||
};
|
||||
|
||||
export const useNodeRealtime = ({
|
||||
onMessage,
|
||||
enabled = true,
|
||||
}: UseNodeRealtimeOptions) => {
|
||||
const [wsConnected, setWsConnected] = useState(false);
|
||||
const [wsConnecting, setWsConnecting] = useState(false);
|
||||
|
||||
const websocketRef = useRef<WebSocket | null>(null);
|
||||
const reconnectTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const reconnectAttemptsRef = useRef(0);
|
||||
const onMessageRef = useRef(onMessage);
|
||||
|
||||
const maxReconnectAttempts = 5;
|
||||
|
||||
useEffect(() => {
|
||||
onMessageRef.current = onMessage;
|
||||
}, [onMessage]);
|
||||
|
||||
const clearReconnectTimer = useCallback(() => {
|
||||
if (reconnectTimerRef.current) {
|
||||
clearTimeout(reconnectTimerRef.current);
|
||||
reconnectTimerRef.current = null;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const disconnect = useCallback(() => {
|
||||
clearReconnectTimer();
|
||||
reconnectAttemptsRef.current = 0;
|
||||
setWsConnected(false);
|
||||
setWsConnecting(false);
|
||||
|
||||
if (!websocketRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
websocketRef.current.onopen = null;
|
||||
websocketRef.current.onmessage = null;
|
||||
websocketRef.current.onerror = null;
|
||||
websocketRef.current.onclose = null;
|
||||
|
||||
if (
|
||||
websocketRef.current.readyState === WebSocket.OPEN ||
|
||||
websocketRef.current.readyState === WebSocket.CONNECTING
|
||||
) {
|
||||
websocketRef.current.close();
|
||||
}
|
||||
|
||||
websocketRef.current = null;
|
||||
}, [clearReconnectTimer]);
|
||||
|
||||
const connect = useCallback(() => {
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
websocketRef.current &&
|
||||
(websocketRef.current.readyState === WebSocket.OPEN ||
|
||||
websocketRef.current.readyState === WebSocket.CONNECTING)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (websocketRef.current) {
|
||||
disconnect();
|
||||
}
|
||||
|
||||
try {
|
||||
setWsConnecting(true);
|
||||
websocketRef.current = new WebSocket(getRealtimeWsUrl());
|
||||
|
||||
websocketRef.current.onopen = () => {
|
||||
reconnectAttemptsRef.current = 0;
|
||||
setWsConnected(true);
|
||||
setWsConnecting(false);
|
||||
};
|
||||
|
||||
websocketRef.current.onmessage = (event) => {
|
||||
try {
|
||||
const parsed = JSON.parse(event.data);
|
||||
|
||||
if (parsed && typeof parsed === "object") {
|
||||
onMessageRef.current(parsed as NodeRealtimeMessage);
|
||||
}
|
||||
} catch {}
|
||||
};
|
||||
|
||||
websocketRef.current.onerror = () => {};
|
||||
|
||||
websocketRef.current.onclose = () => {
|
||||
websocketRef.current = null;
|
||||
setWsConnected(false);
|
||||
setWsConnecting(false);
|
||||
|
||||
if (!enabled || reconnectAttemptsRef.current >= maxReconnectAttempts) {
|
||||
return;
|
||||
}
|
||||
|
||||
reconnectAttemptsRef.current += 1;
|
||||
reconnectTimerRef.current = setTimeout(() => {
|
||||
reconnectTimerRef.current = null;
|
||||
connect();
|
||||
}, 3000 * reconnectAttemptsRef.current);
|
||||
};
|
||||
} catch {
|
||||
setWsConnected(false);
|
||||
setWsConnecting(false);
|
||||
}
|
||||
}, [disconnect, enabled]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
connect();
|
||||
|
||||
return () => {
|
||||
disconnect();
|
||||
};
|
||||
}, [connect, disconnect, enabled]);
|
||||
|
||||
return {
|
||||
wsConnected,
|
||||
wsConnecting,
|
||||
reconnectRealtime: connect,
|
||||
disconnectRealtime: disconnect,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user