init: 初始化

This commit is contained in:
Montia37
2025-08-13 04:32:05 +08:00
commit af6f7b1d09
343 changed files with 9919 additions and 0 deletions

139
src/hooks/useLoadCharts.ts Normal file
View File

@@ -0,0 +1,139 @@
import { useState, useEffect, useMemo } from "react";
import { useNodeData } from "@/contexts/NodeDataContext";
import type { HistoryRecord, NodeData, NodeStats } from "@/types/node";
import { useLiveData } from "@/contexts/LiveDataContext";
export const useLoadCharts = (node: NodeData | null, hours: number) => {
const { getLoadHistory, getRecentLoadHistory } = useNodeData();
const { liveData } = useLiveData();
const [historicalData, setHistoricalData] = useState<HistoryRecord[]>([]);
const [realtimeData, setRealtimeData] = useState<HistoryRecord[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const isRealtime = hours === 0;
// Fetch historical data
useEffect(() => {
if (isRealtime || !node?.uuid) return;
const fetchHistoricalData = async () => {
setLoading(true);
setError(null);
try {
const data = await getLoadHistory(node.uuid, hours);
setHistoricalData(data?.records || []);
setRealtimeData([]); // Clear realtime data
} catch (err: any) {
setError(err.message || "Failed to fetch historical data");
} finally {
setLoading(false);
}
};
fetchHistoricalData();
}, [node?.uuid, hours, getLoadHistory, isRealtime]);
// Fetch initial real-time data and handle WebSocket updates
useEffect(() => {
if (!isRealtime || !node?.uuid) return;
const fetchInitialRealtimeData = async () => {
setLoading(true);
setError(null);
try {
const data = await getRecentLoadHistory(node.uuid);
setRealtimeData(data?.records || []);
setHistoricalData([]); // Clear historical data
} catch (err: any) {
setError(err.message || "Failed to fetch initial real-time data");
} finally {
setLoading(false);
}
};
fetchInitialRealtimeData();
}, [node?.uuid, getRecentLoadHistory, isRealtime]);
// Separate effect for WebSocket updates
useEffect(() => {
if (!isRealtime || !node?.uuid || !liveData?.data[node.uuid]) return;
const stats: NodeStats = liveData.data[node.uuid];
const newRecord: HistoryRecord = {
client: node.uuid,
time: new Date(stats.updated_at).toISOString(),
cpu: stats.cpu.usage,
ram: stats.ram.used,
disk: stats.disk.used,
load: stats.load.load1,
net_in: stats.network.down,
net_out: stats.network.up,
process: stats.process,
connections: stats.connections.tcp,
gpu: 0,
ram_total: stats.ram.total,
swap: stats.swap.used,
swap_total: stats.swap.total,
temp: 0,
disk_total: stats.disk.total,
net_total_up: stats.network.totalUp,
net_total_down: stats.network.totalDown,
connections_udp: stats.connections.udp,
};
setRealtimeData((prevHistory) => {
if (
prevHistory.length > 0 &&
new Date(prevHistory[prevHistory.length - 1].time).getTime() ===
new Date(newRecord.time).getTime()
) {
return prevHistory;
}
const updatedHistory = [...prevHistory, newRecord];
return updatedHistory.length > 600
? updatedHistory.slice(updatedHistory.length - 600)
: updatedHistory;
});
}, [liveData, node?.uuid, isRealtime]);
const historicalChartData = useMemo(() => {
return historicalData.map((record) => ({
time: new Date(record.time).getTime(),
cpu: record.cpu,
ram: record.ram,
disk: record.disk,
load: record.load,
net_out: record.net_out,
net_in: record.net_in,
connections: record.connections,
process: record.process,
swap: record.swap,
connections_udp: record.connections_udp,
}));
}, [historicalData]);
const realtimeChartData = useMemo(() => {
return realtimeData.map((record) => ({
time: new Date(record.time).getTime(),
cpu: record.cpu,
ram: record.ram,
disk: record.disk,
load: record.load,
net_out: record.net_out,
net_in: record.net_in,
connections: record.connections,
process: record.process,
swap: record.swap,
connections_udp: record.connections_udp,
}));
}, [realtimeData]);
const chartData = isRealtime ? realtimeChartData : historicalChartData;
return {
loading,
error,
chartData,
};
};

21
src/hooks/useMobile.ts Normal file
View File

@@ -0,0 +1,21 @@
import * as React from "react";
const MOBILE_BREAKPOINT = 768;
export function useIsMobile() {
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(
undefined
);
React.useEffect(() => {
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
const onChange = () => {
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
};
mql.addEventListener("change", onChange);
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
return () => mql.removeEventListener("change", onChange);
}, []);
return !!isMobile;
}

View File

@@ -0,0 +1,68 @@
import { useMemo } from "react";
import type { NodeWithStatus } from "@/types/node";
import { formatPrice } from "@/utils";
export const useNodeCommons = (node: NodeWithStatus) => {
const { stats, isOnline } = useMemo(() => {
return {
stats: node.stats,
isOnline: node.status === "online",
};
}, [node]);
const price = formatPrice(node.price, node.currency, node.billing_cycle);
const cpuUsage = stats && isOnline ? stats.cpu.usage : 0;
const memUsage =
stats && isOnline && stats.ram.total > 0
? (stats.ram.used / stats.ram.total) * 100
: 0;
const swapUsage =
stats && isOnline && stats.swap.total > 0
? (stats.swap.used / stats.swap.total) * 100
: 0;
const diskUsage =
stats && isOnline && stats.disk.total > 0
? (stats.disk.used / stats.disk.total) * 100
: 0;
const load = stats
? `${stats.load.load1.toFixed(2)} | ${stats.load.load5.toFixed(
2
)} | ${stats.load.load15.toFixed(2)}`
: "N/A";
const daysLeft = node.expired_at
? Math.ceil(
(new Date(node.expired_at).getTime() - new Date().getTime()) /
(1000 * 60 * 60 * 24)
)
: null;
const tagList = [
price,
`${daysLeft && daysLeft < 0 ? "已过期" : ""}${
daysLeft && daysLeft >= 0 && daysLeft < 36500
? "余 " + daysLeft + " 天"
: ""
}`,
...(typeof node.tags === "string"
? node.tags
.split(";")
.map((tag) => tag.trim())
.filter(Boolean)
: []),
];
return {
stats,
isOnline,
tagList,
cpuUsage,
memUsage,
swapUsage,
diskUsage,
load,
daysLeft,
};
};

42
src/hooks/usePingChart.ts Normal file
View File

@@ -0,0 +1,42 @@
import { useState, useEffect } from "react";
import { useNodeData } from "@/contexts/NodeDataContext";
import type { PingHistoryResponse, NodeData } from "@/types/node";
export const usePingChart = (node: NodeData | null, hours: number) => {
const { getPingHistory } = useNodeData();
const [pingHistory, setPingHistory] = useState<PingHistoryResponse | null>(
null
);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
if (!node?.uuid) {
setPingHistory(null);
setLoading(false);
return;
}
setLoading(true);
setError(null);
const fetchHistory = async () => {
try {
const data = await getPingHistory(node.uuid, hours);
setPingHistory(data);
} catch (err: any) {
setError(err.message || "Failed to fetch history data");
} finally {
setLoading(false);
}
};
fetchHistory();
}, [node?.uuid, hours, getPingHistory]);
return {
loading,
error,
pingHistory,
};
};

40
src/hooks/useTheme.ts Normal file
View File

@@ -0,0 +1,40 @@
import { useState, useEffect } from "react";
type Theme = "light" | "dark" | "system";
export const useTheme = () => {
const [theme, setTheme] = useState<Theme>(() => {
const storedTheme = localStorage.getItem("appearance");
if (
storedTheme === "light" ||
storedTheme === "dark" ||
storedTheme === "system"
) {
return storedTheme;
}
return "system";
});
useEffect(() => {
const root = window.document.documentElement;
root.classList.remove("light", "dark");
if (theme === "system") {
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
.matches
? "dark"
: "light";
root.classList.add(systemTheme);
} else {
root.classList.add(theme);
}
localStorage.setItem("appearance", theme);
}, [theme]);
const toggleTheme = () => {
const newTheme = theme === "light" ? "dark" : "light";
setTheme(newTheme);
};
return { theme, toggleTheme };
};