mirror of
https://github.com/fankes/komari-theme-purcarte.git
synced 2025-10-20 12:29:22 +08:00
init: 初始化
This commit is contained in:
139
src/hooks/useLoadCharts.ts
Normal file
139
src/hooks/useLoadCharts.ts
Normal 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
21
src/hooks/useMobile.ts
Normal 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;
|
||||
}
|
68
src/hooks/useNodeCommons.ts
Normal file
68
src/hooks/useNodeCommons.ts
Normal 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
42
src/hooks/usePingChart.ts
Normal 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
40
src/hooks/useTheme.ts
Normal 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 };
|
||||
};
|
Reference in New Issue
Block a user