feat: 添加私有状态处理及私有页面组件

This commit is contained in:
Montia37
2025-09-10 10:57:23 +08:00
parent 250ee5c88f
commit c6e5dc6d39
7 changed files with 104 additions and 30 deletions

View File

@@ -11,7 +11,7 @@ import type { NodeData, PublicInfo, HistoryRecord } from "../types/node";
// The core logic from the original useNodeData.ts, now kept internal to this file. // The core logic from the original useNodeData.ts, now kept internal to this file.
function useNodesInternal() { function useNodesInternal() {
const [staticNodes, setStaticNodes] = useState<NodeData[]>([]); const [staticNodes, setStaticNodes] = useState<NodeData[] | "private">([]);
const [publicSettings, setPublicSettings] = useState<PublicInfo | null>(null); const [publicSettings, setPublicSettings] = useState<PublicInfo | null>(null);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
@@ -25,8 +25,14 @@ function useNodesInternal() {
apiService.getNodes(), apiService.getNodes(),
apiService.getPublicSettings(), apiService.getPublicSettings(),
]); ]);
const sortedNodes = nodeData.sort((a, b) => a.weight - b.weight);
setStaticNodes(sortedNodes); if (nodeData === "private") {
setStaticNodes("private");
} else {
const sortedNodes = nodeData.sort((a, b) => a.weight - b.weight);
setStaticNodes(sortedNodes);
}
setPublicSettings(publicSettings); setPublicSettings(publicSettings);
} catch (err) { } catch (err) {
setError(err instanceof Error ? err.message : "获取节点数据失败"); setError(err instanceof Error ? err.message : "获取节点数据失败");
@@ -115,15 +121,21 @@ function useNodesInternal() {
const getNodesByGroup = useCallback( const getNodesByGroup = useCallback(
(group: string) => { (group: string) => {
return staticNodes.filter((node) => node.group === group); if (Array.isArray(staticNodes)) {
return staticNodes.filter((node) => node.group === group);
}
return [];
}, },
[staticNodes] [staticNodes]
); );
const getGroups = useCallback(() => { const getGroups = useCallback(() => {
return Array.from( if (Array.isArray(staticNodes)) {
new Set(staticNodes.map((node) => node.group).filter(Boolean)) return Array.from(
); new Set(staticNodes.map((node) => node.group).filter(Boolean))
);
}
return [];
}, [staticNodes]); }, [staticNodes]);
return { return {

View File

@@ -17,11 +17,13 @@ import Loading from "./components/loading";
const HomePage = lazy(() => import("@/pages/Home")); const HomePage = lazy(() => import("@/pages/Home"));
const InstancePage = lazy(() => import("@/pages/instance")); const InstancePage = lazy(() => import("@/pages/instance"));
const NotFoundPage = lazy(() => import("@/pages/NotFound")); const NotFoundPage = lazy(() => import("@/pages/NotFound"));
const PrivatePage = lazy(() => import("@/pages/Private"));
import { useConfigItem } from "@/config"; import { useConfigItem } from "@/config";
// 内部应用组件,在 ConfigProvider 内部使用配置 // 内部应用组件,在 ConfigProvider 内部使用配置
export const AppContent = () => { export const AppContent = () => {
const { nodes } = useNodeData();
const { appearance, color } = useTheme(); const { appearance, color } = useTheme();
const [searchTerm, setSearchTerm] = useState(""); const [searchTerm, setSearchTerm] = useState("");
const enableVideoBackground = useConfigItem("enableVideoBackground"); const enableVideoBackground = useConfigItem("enableVideoBackground");
@@ -46,19 +48,23 @@ export const AppContent = () => {
<div className="min-h-screen flex flex-col text-sm"> <div className="min-h-screen flex flex-col text-sm">
<Header searchTerm={searchTerm} setSearchTerm={setSearchTerm} /> <Header searchTerm={searchTerm} setSearchTerm={setSearchTerm} />
<Suspense fallback={<Loading />}> <Suspense fallback={<Loading />}>
<Routes> {nodes === "private" ? (
<Route <PrivatePage />
path="/" ) : (
element={ <Routes>
<HomePage <Route
searchTerm={searchTerm} path="/"
setSearchTerm={setSearchTerm} element={
/> <HomePage
} searchTerm={searchTerm}
/> setSearchTerm={setSearchTerm}
<Route path="/instance/:uuid" element={<InstancePage />} /> />
<Route path="*" element={<NotFoundPage />} /> }
</Routes> />
<Route path="/instance/:uuid" element={<InstancePage />} />
<Route path="*" element={<NotFoundPage />} />
</Routes>
)}
</Suspense> </Suspense>
<Footer /> <Footer />
</div> </div>

View File

@@ -44,7 +44,7 @@ const HomePage: React.FC<HomePageProps> = ({ searchTerm, setSearchTerm }) => {
selectTrafficProgressStyle, selectTrafficProgressStyle,
} = useAppConfig(); } = useAppConfig();
const combinedNodes = useMemo<NodeWithStatus[]>(() => { const combinedNodes = useMemo<NodeWithStatus[]>(() => {
if (!staticNodes) return []; if (!staticNodes || staticNodes === "private") return [];
return staticNodes.map((node) => { return staticNodes.map((node) => {
const isOnline = liveData?.online.includes(node.uuid) ?? false; const isOnline = liveData?.online.includes(node.uuid) ?? false;
const stats = isOnline ? liveData?.data[node.uuid] : undefined; const stats = isOnline ? liveData?.data[node.uuid] : undefined;

View File

@@ -12,7 +12,7 @@ export default function NotFound() {
const navigate = useNavigate(); const navigate = useNavigate();
return ( return (
<div className="flex flex-grow items-center justify-center"> <div className="fixed inset-0 flex items-center justify-center">
<Card className="w-full max-w-md"> <Card className="w-full max-w-md">
<CardHeader> <CardHeader>
<CardTitle className="text-2xl font-bold">404 - Not Found</CardTitle> <CardTitle className="text-2xl font-bold">404 - Not Found</CardTitle>

29
src/pages/Private.tsx Normal file
View File

@@ -0,0 +1,29 @@
import { Button } from "@/components/ui/button";
import {
Card,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { useNavigate } from "react-router-dom";
export default function Private() {
const navigate = useNavigate();
return (
<div className="fixed inset-0 flex items-center justify-center">
<Card className="w-full max-w-md">
<CardHeader>
<CardTitle className="text-2xl font-bold"></CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardFooter>
<Button onClick={() => navigate("/admin")} className="w-full">
</Button>
</CardFooter>
</Card>
</div>
);
}

View File

@@ -75,8 +75,10 @@ const InstancePage = () => {
}, [timeRanges, maxRecordPreserveTime]); }, [timeRanges, maxRecordPreserveTime]);
useEffect(() => { useEffect(() => {
const foundNode = staticNodes.find((n) => n.uuid === uuid); if (Array.isArray(staticNodes)) {
setStaticNode(foundNode || null); const foundNode = staticNodes.find((n: NodeData) => n.uuid === uuid);
setStaticNode(foundNode || null);
}
}, [staticNodes, uuid]); }, [staticNodes, uuid]);
const node = useMemo(() => { const node = useMemo(() => {

View File

@@ -16,28 +16,53 @@ class ApiService {
this.baseUrl = ""; this.baseUrl = "";
} }
async get<T>(endpoint: string): Promise<ApiResponse<T>> { async get<T>(
endpoint: string
): Promise<
| ApiResponse<T>
| { status: number }
| { status: string; message: string; data: any }
> {
try { try {
const response = await fetch(`${this.baseUrl}${endpoint}`); const response = await fetch(`${this.baseUrl}${endpoint}`);
if (!response.ok) { if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`); if (response.status === 401) {
return { status: 401 };
}
// 对于其他 HTTP 错误,直接返回错误对象,而不是抛出异常
return {
status: "error",
message: `HTTP error! status: ${response.status}`,
data: null as any,
};
} }
const data = await response.json(); const data = await response.json();
return data; return data;
} catch (error) { } catch (error) {
console.error("API request failed:", error); // 这个 catch 块现在只处理网络层面的错误
console.error("API request failed (network error):", error);
return { return {
status: "error", status: "error",
message: error instanceof Error ? error.message : "Unknown error", message:
error instanceof Error ? error.message : "Unknown network error",
data: null as any, data: null as any,
}; };
} }
} }
// 获取所有节点信息 // 获取所有节点信息
async getNodes(): Promise<NodeData[]> { async getNodes(): Promise<NodeData[] | "private"> {
const response = await this.get<NodeData[]>("/api/nodes"); const response = await this.get<NodeData[]>("/api/nodes");
return response.status === "success" ? response.data : []; // 检查是否为私有状态
if ("status" in response && response.status === 401) {
return "private";
}
// 检查是否为成功的 API 响应
if ("status" in response && response.status === "success") {
return (response as ApiResponse<NodeData[]>).data;
}
// 其他情况返回空数组
return [];
} }
// 获取指定节点的最近状态 // 获取指定节点的最近状态