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.
function useNodesInternal() {
const [staticNodes, setStaticNodes] = useState<NodeData[]>([]);
const [staticNodes, setStaticNodes] = useState<NodeData[] | "private">([]);
const [publicSettings, setPublicSettings] = useState<PublicInfo | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
@@ -25,8 +25,14 @@ function useNodesInternal() {
apiService.getNodes(),
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);
} catch (err) {
setError(err instanceof Error ? err.message : "获取节点数据失败");
@@ -115,15 +121,21 @@ function useNodesInternal() {
const getNodesByGroup = useCallback(
(group: string) => {
return staticNodes.filter((node) => node.group === group);
if (Array.isArray(staticNodes)) {
return staticNodes.filter((node) => node.group === group);
}
return [];
},
[staticNodes]
);
const getGroups = useCallback(() => {
return Array.from(
new Set(staticNodes.map((node) => node.group).filter(Boolean))
);
if (Array.isArray(staticNodes)) {
return Array.from(
new Set(staticNodes.map((node) => node.group).filter(Boolean))
);
}
return [];
}, [staticNodes]);
return {

View File

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

View File

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

View File

@@ -12,7 +12,7 @@ export default function NotFound() {
const navigate = useNavigate();
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">
<CardHeader>
<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]);
useEffect(() => {
const foundNode = staticNodes.find((n) => n.uuid === uuid);
setStaticNode(foundNode || null);
if (Array.isArray(staticNodes)) {
const foundNode = staticNodes.find((n: NodeData) => n.uuid === uuid);
setStaticNode(foundNode || null);
}
}, [staticNodes, uuid]);
const node = useMemo(() => {

View File

@@ -16,28 +16,53 @@ class ApiService {
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 {
const response = await fetch(`${this.baseUrl}${endpoint}`);
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();
return data;
} catch (error) {
console.error("API request failed:", error);
// 这个 catch 块现在只处理网络层面的错误
console.error("API request failed (network error):", error);
return {
status: "error",
message: error instanceof Error ? error.message : "Unknown error",
message:
error instanceof Error ? error.message : "Unknown network error",
data: null as any,
};
}
}
// 获取所有节点信息
async getNodes(): Promise<NodeData[]> {
async getNodes(): Promise<NodeData[] | "private"> {
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 [];
}
// 获取指定节点的最近状态