- 新增 model_deploy 与 model_operations 双库查询,支持模型列表、模型详情、单月监控结果和运维工作台真实接口。 - 关闭运维模块生产 Mock 数据,补充缺表/空数据降级、工作台空状态和首条纵向链路测试。 - 按业务团队、模型团队、管理员权限矩阵接入菜单、页面、操作按钮和后端接口权限校验,支持 business_team 角色。 - 更新工作台布局、深色欢迎卡片、全宽页面适配、顶部回退,以及权限分组展示。 - 新增架构实现基线、周目标完成情况和角色权限矩阵初始化 SQL 文档。
167 lines
6.2 KiB
TypeScript
167 lines
6.2 KiB
TypeScript
import { createContext, type ReactNode, useCallback, useContext, useEffect, useMemo, useState } from "react";
|
|
import { 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,
|
|
getOperationsWorkbench,
|
|
listOperationsModels,
|
|
OperationsApiError,
|
|
type OperationsWorkbenchDto,
|
|
operationsApiMode,
|
|
type OperationsApiMode,
|
|
} from "~/services/operationsApi";
|
|
import type { ModelRecord, MonitoringRow } from "./modelData";
|
|
|
|
type OperationsDataContextValue = {
|
|
models: ModelRecord[];
|
|
workbench: OperationsWorkbenchDto;
|
|
loading: boolean;
|
|
error: string | null;
|
|
source: OperationsApiMode;
|
|
reload: () => Promise<void>;
|
|
};
|
|
|
|
const OperationsDataContext = createContext<OperationsDataContextValue | null>(null);
|
|
|
|
const EMPTY_WORKBENCH: OperationsWorkbenchDto = {
|
|
role: "model_team",
|
|
alerts: [],
|
|
kpis: [],
|
|
todos: [],
|
|
watches: [],
|
|
activities: [],
|
|
};
|
|
|
|
export function OperationsDataProvider({ children }: { children: ReactNode }) {
|
|
const { currentWorkspace } = useAuth();
|
|
const workspaceId = currentWorkspace?.workspace_id;
|
|
const [models, setModels] = useState<ModelRecord[]>([]);
|
|
const [workbench, setWorkbench] = useState<OperationsWorkbenchDto>(EMPTY_WORKBENCH);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
const load = useCallback(async (signal?: AbortSignal) => {
|
|
setLoading(true);
|
|
setError(null);
|
|
try {
|
|
const [modelsResult, workbenchResult] = await Promise.allSettled([
|
|
listOperationsModels({ workspaceId, signal }),
|
|
getOperationsWorkbench(workspaceId, signal),
|
|
]);
|
|
if (modelsResult.status === "rejected") throw modelsResult.reason;
|
|
setModels(modelsResult.value);
|
|
setWorkbench(workbenchResult.status === "fulfilled" ? workbenchResult.value : EMPTY_WORKBENCH);
|
|
} 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,
|
|
workbench,
|
|
loading,
|
|
error,
|
|
source: operationsApiMode,
|
|
reload: () => load(),
|
|
}), [error, load, loading, models, workbench]);
|
|
|
|
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).catch((cause) => {
|
|
if (cause instanceof OperationsApiError && cause.code === "MONITOR_RESULT_NOT_FOUND") {
|
|
return null;
|
|
}
|
|
throw cause;
|
|
}),
|
|
]).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, reload } = useOperationsData();
|
|
if (loading) {
|
|
return (
|
|
<section className="h-full overflow-auto bg-bg p-6" aria-busy="true" aria-label="正在加载运维数据">
|
|
<div className="w-full 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">当前数据源:真实接口</p>
|
|
<Button className="mt-5" onClick={() => void reload()}><RefreshCw />重新加载</Button>
|
|
</CardContent>
|
|
</Card>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
return children;
|
|
}
|