mirror of
https://github.com/fankes/komari-theme-purcarte.git
synced 2025-12-13 04:51:28 +08:00
feat: 添加私有状态处理及私有页面组件
This commit is contained in:
@@ -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 {
|
||||||
|
|||||||
32
src/main.tsx
32
src/main.tsx
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
29
src/pages/Private.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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(() => {
|
||||||
|
|||||||
@@ -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 [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取指定节点的最近状态
|
// 获取指定节点的最近状态
|
||||||
|
|||||||
Reference in New Issue
Block a user