mirror of
https://github.com/fankes/komari-theme-purcarte.git
synced 2025-10-18 11:29:22 +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.
|
||||
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 {
|
||||
|
32
src/main.tsx
32
src/main.tsx
@@ -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>
|
||||
|
@@ -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;
|
||||
|
@@ -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
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]);
|
||||
|
||||
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(() => {
|
||||
|
@@ -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 [];
|
||||
}
|
||||
|
||||
// 获取指定节点的最近状态
|
||||
|
Reference in New Issue
Block a user