149 lines
6.1 KiB
TypeScript
149 lines
6.1 KiB
TypeScript
import { createContext, type ReactNode, useCallback, useContext, useEffect, useMemo, useState } from "react";
|
|
import { Database, RefreshCw, TriangleAlert } from "lucide-react";
|
|
|
|
import { Button } from "~/components/ui/button";
|
|
import { Card, CardContent } from "~/components/ui/card";
|
|
import { Skeleton } from "~/components/ui/skeleton";
|
|
import { useAuth } from "~/context/AuthContext";
|
|
import {
|
|
getMonthlyMonitoringResult,
|
|
getOperationsModel,
|
|
listOperationsModels,
|
|
operationsApiMode,
|
|
type OperationsApiMode,
|
|
} from "~/services/operationsApi";
|
|
import type { ModelRecord, MonitoringRow } from "./modelData";
|
|
|
|
type OperationsDataContextValue = {
|
|
models: ModelRecord[];
|
|
loading: boolean;
|
|
error: string | null;
|
|
source: OperationsApiMode;
|
|
reload: () => Promise<void>;
|
|
};
|
|
|
|
const OperationsDataContext = createContext<OperationsDataContextValue | null>(null);
|
|
|
|
export function OperationsDataProvider({ children }: { children: ReactNode }) {
|
|
const { currentWorkspace } = useAuth();
|
|
const workspaceId = currentWorkspace?.workspace_id;
|
|
const [models, setModels] = useState<ModelRecord[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
const load = useCallback(async (signal?: AbortSignal) => {
|
|
setLoading(true);
|
|
setError(null);
|
|
try {
|
|
setModels(await listOperationsModels({ workspaceId, signal }));
|
|
} catch (cause) {
|
|
if (cause instanceof DOMException && cause.name === "AbortError") return;
|
|
setModels([]);
|
|
setError(cause instanceof Error ? cause.message : "模型数据加载失败");
|
|
} finally {
|
|
if (!signal?.aborted) setLoading(false);
|
|
}
|
|
}, [workspaceId]);
|
|
|
|
useEffect(() => {
|
|
const controller = new AbortController();
|
|
void load(controller.signal);
|
|
return () => controller.abort();
|
|
}, [load]);
|
|
|
|
const value = useMemo<OperationsDataContextValue>(() => ({
|
|
models,
|
|
loading,
|
|
error,
|
|
source: operationsApiMode,
|
|
reload: () => load(),
|
|
}), [error, load, loading, models]);
|
|
|
|
return <OperationsDataContext.Provider value={value}>{children}</OperationsDataContext.Provider>;
|
|
}
|
|
|
|
export function useOperationsData(): OperationsDataContextValue {
|
|
const context = useContext(OperationsDataContext);
|
|
if (!context) throw new Error("useOperationsData 必须在 OperationsDataProvider 内使用");
|
|
return context;
|
|
}
|
|
|
|
export function useOperationsModelDetail(modelId: string, month: string) {
|
|
const { currentWorkspace } = useAuth();
|
|
const workspaceId = currentWorkspace?.workspace_id;
|
|
const [model, setModel] = useState<ModelRecord | null>(null);
|
|
const [monitoringResult, setMonitoringResult] = useState<MonitoringRow | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [reloadKey, setReloadKey] = useState(0);
|
|
|
|
useEffect(() => {
|
|
const controller = new AbortController();
|
|
setLoading(true);
|
|
setError(null);
|
|
void Promise.all([
|
|
getOperationsModel(modelId, workspaceId, controller.signal),
|
|
getMonthlyMonitoringResult(modelId, month, workspaceId, controller.signal),
|
|
]).then(([modelValue, resultValue]) => {
|
|
setModel(modelValue);
|
|
setMonitoringResult(resultValue);
|
|
}).catch((cause) => {
|
|
if (cause instanceof DOMException && cause.name === "AbortError") return;
|
|
setError(cause instanceof Error ? cause.message : "模型监控详情加载失败");
|
|
}).finally(() => {
|
|
if (!controller.signal.aborted) setLoading(false);
|
|
});
|
|
return () => controller.abort();
|
|
}, [modelId, month, reloadKey, workspaceId]);
|
|
|
|
return {
|
|
model,
|
|
monitoringResult,
|
|
loading,
|
|
error,
|
|
reload: () => setReloadKey((value) => value + 1),
|
|
};
|
|
}
|
|
|
|
export function OperationsDataBoundary({ children }: { children: ReactNode }) {
|
|
const { loading, error, models, reload, source } = useOperationsData();
|
|
if (loading) {
|
|
return (
|
|
<section className="h-full overflow-auto bg-bg p-6" aria-busy="true" aria-label="正在加载运维数据">
|
|
<div className="mx-auto max-w-screen-2xl space-y-6">
|
|
<div className="space-y-3"><Skeleton className="h-3 w-48" /><Skeleton className="h-8 w-80" /><Skeleton className="h-4 w-[36rem]" /></div>
|
|
<Skeleton className="h-40 w-full rounded-4xl" />
|
|
<div className="grid grid-cols-4 gap-4">{Array.from({ length: 4 }, (_, index) => <Skeleton className="h-32 rounded-4xl" key={index} />)}</div>
|
|
<div className="grid grid-cols-2 gap-6"><Skeleton className="h-72 rounded-4xl" /><Skeleton className="h-72 rounded-4xl" /></div>
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
if (error) {
|
|
return (
|
|
<section className="grid h-full place-items-center bg-bg p-6">
|
|
<Card className="w-full max-w-xl">
|
|
<CardContent className="flex flex-col items-center py-12 text-center">
|
|
<span className="grid size-12 place-items-center rounded-2xl bg-danger-soft text-danger"><TriangleAlert className="size-6" /></span>
|
|
<h2 className="mt-4 text-xl font-bold text-foreground">运维数据加载失败</h2>
|
|
<p className="mt-2 max-w-md text-sm leading-6 text-muted-foreground">{error}</p>
|
|
<p className="mt-1 text-xs text-muted-foreground">当前数据源:{source === "api" ? "真实接口" : "前端 Mock"}</p>
|
|
<Button className="mt-5" onClick={() => void reload()}><RefreshCw />重新加载</Button>
|
|
</CardContent>
|
|
</Card>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
if (!models.length) {
|
|
return (
|
|
<section className="grid h-full place-items-center bg-bg p-6">
|
|
<Card className="w-full max-w-xl"><CardContent className="flex flex-col items-center py-12 text-center"><span className="grid size-12 place-items-center rounded-2xl bg-brand-soft text-primary"><Database className="size-6" /></span><h2 className="mt-4 text-xl font-bold text-foreground">暂无模型数据</h2><p className="mt-2 text-sm text-muted-foreground">请确认模型平台已同步模型信息,或调整接口环境配置。</p><Button className="mt-5" variant="outline" onClick={() => void reload()}><RefreshCw />重新加载</Button></CardContent></Card>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
return children;
|
|
}
|