feat: 接入运维真实数据与三角色权限链路
- 新增 model_deploy 与 model_operations 双库查询,支持模型列表、模型详情、单月监控结果和运维工作台真实接口。 - 关闭运维模块生产 Mock 数据,补充缺表/空数据降级、工作台空状态和首条纵向链路测试。 - 按业务团队、模型团队、管理员权限矩阵接入菜单、页面、操作按钮和后端接口权限校验,支持 business_team 角色。 - 更新工作台布局、深色欢迎卡片、全宽页面适配、顶部回退,以及权限分组展示。 - 新增架构实现基线、周目标完成情况和角色权限矩阵初始化 SQL 文档。
This commit is contained in:
@@ -1,16 +1,3 @@
|
||||
export type DemoUser = {
|
||||
userId: string;
|
||||
userName: string;
|
||||
username: string;
|
||||
roleCode: "admin" | "developer";
|
||||
roleName: string;
|
||||
};
|
||||
|
||||
export type DemoWorkspace = {
|
||||
workspaceId: string;
|
||||
workspaceName: string;
|
||||
};
|
||||
|
||||
export function createUuid(): string {
|
||||
const cryptoApi = globalThis.crypto;
|
||||
if (typeof cryptoApi?.randomUUID === "function") {
|
||||
@@ -40,58 +27,6 @@ export function createUuid(): string {
|
||||
].join("-");
|
||||
}
|
||||
|
||||
export const demoUsers: DemoUser[] = [
|
||||
{ userId: "0000000000RF6FG1SDBXG59S13", userName: "张三", username: "admin-zhang", roleCode: "admin", roleName: "管理员" },
|
||||
{ userId: "0000000000H2QYCGPCWQM1JSGS", userName: "李四", username: "admin-li", roleCode: "admin", roleName: "管理员" },
|
||||
{ userId: "0000000000RWG40ESZPGJT629J", userName: "王五", username: "dev-wang", roleCode: "developer", roleName: "开发人员" },
|
||||
{ userId: "00000000004CQV7WASJA6N6FW4", userName: "赵六", username: "dev-zhao", roleCode: "developer", roleName: "开发人员" },
|
||||
];
|
||||
|
||||
export const demoWorkspaces: DemoWorkspace[] = [
|
||||
{ workspaceId: "00000000000BM630VT9ARVFZPC", workspaceName: "模型开发 Workspace" },
|
||||
{ workspaceId: "0000000000AE0NC0V5T424KK86", workspaceName: "风险验证 Workspace" },
|
||||
];
|
||||
|
||||
function readStoredContext(): Partial<{
|
||||
userId: string;
|
||||
workspaceId: string;
|
||||
}> {
|
||||
if (typeof window === "undefined") return {};
|
||||
try {
|
||||
return JSON.parse(
|
||||
window.localStorage.getItem("model-platform-demo-context") ?? "{}",
|
||||
) as Partial<{ userId: string; workspaceId: string }>;
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
const storedContext = readStoredContext();
|
||||
const initialUser = demoUsers.find((item) => item.userId === storedContext.userId)
|
||||
?? demoUsers[0];
|
||||
const initialWorkspace = demoWorkspaces.find(
|
||||
(item) => item.workspaceId === storedContext.workspaceId,
|
||||
) ?? demoWorkspaces[0];
|
||||
|
||||
export const demoContext = {
|
||||
...initialUser,
|
||||
...initialWorkspace,
|
||||
};
|
||||
|
||||
export function setDemoContext(input: {
|
||||
user?: DemoUser;
|
||||
workspace?: DemoWorkspace;
|
||||
}): void {
|
||||
if (input.user) Object.assign(demoContext, input.user);
|
||||
if (input.workspace) Object.assign(demoContext, input.workspace);
|
||||
if (typeof window !== "undefined") {
|
||||
window.localStorage.setItem("model-platform-demo-context", JSON.stringify({
|
||||
userId: demoContext.userId,
|
||||
workspaceId: demoContext.workspaceId,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
// API client for the platform backend.
|
||||
//
|
||||
// All endpoints that take a workspace context require the caller to
|
||||
@@ -111,7 +46,7 @@ export type Employee = {
|
||||
display_name: string;
|
||||
email: string | null;
|
||||
status: "active" | "disabled" | "locked";
|
||||
role_code: "admin" | "developer";
|
||||
role_code: string;
|
||||
role_name: string;
|
||||
created_at: string;
|
||||
};
|
||||
|
||||
@@ -245,7 +245,7 @@ export type WorkspaceBoundApi = {
|
||||
display_name: string;
|
||||
email?: string | undefined;
|
||||
password: string;
|
||||
role_code?: "admin" | "developer";
|
||||
role_code?: string;
|
||||
},
|
||||
) => Promise<Employee>;
|
||||
updateEmployee: (
|
||||
@@ -257,7 +257,7 @@ export type WorkspaceBoundApi = {
|
||||
input: {
|
||||
display_name?: string;
|
||||
email?: string | null;
|
||||
role_code?: "admin" | "developer";
|
||||
role_code?: string;
|
||||
status?: "active" | "disabled" | "locked";
|
||||
},
|
||||
) => Promise<Employee>;
|
||||
|
||||
@@ -25,7 +25,7 @@ export async function createPlatformEmployee(
|
||||
display_name: string;
|
||||
email?: string | undefined;
|
||||
password: string;
|
||||
role_code?: "admin" | "developer";
|
||||
role_code?: string;
|
||||
},
|
||||
): Promise<Employee> {
|
||||
return apiRequest<Employee>(
|
||||
@@ -39,7 +39,7 @@ export async function updatePlatformEmployee(
|
||||
input: {
|
||||
display_name?: string;
|
||||
email?: string | null;
|
||||
role_code?: "admin" | "developer";
|
||||
role_code?: string;
|
||||
status?: "active" | "disabled" | "locked";
|
||||
},
|
||||
): Promise<Employee> {
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
import {
|
||||
MODELS,
|
||||
monitoringRows,
|
||||
type ModelCategoryId,
|
||||
type ModelRecord,
|
||||
type ModelStatus,
|
||||
type MonitoringRow,
|
||||
type RankingStatus,
|
||||
import type {
|
||||
ModelCategoryId,
|
||||
ModelRecord,
|
||||
ModelStatus,
|
||||
MonitoringRow,
|
||||
RankingStatus,
|
||||
} from "~/features/operations/modelData";
|
||||
|
||||
export type OperationsApiMode = "mock" | "api";
|
||||
export type OperationsApiMode = "api";
|
||||
|
||||
export type OperationsModelListParams = {
|
||||
workspaceId?: string;
|
||||
@@ -49,6 +47,41 @@ export type MonthlyMonitoringResultDto = {
|
||||
secondary_hits_6m: number;
|
||||
};
|
||||
|
||||
export type WorkbenchTone = "normal" | "warning" | "danger" | "primary";
|
||||
|
||||
export type OperationsWorkbenchKpi = {
|
||||
key: string;
|
||||
label: string;
|
||||
value: number;
|
||||
detail: string;
|
||||
target: string;
|
||||
tone: WorkbenchTone;
|
||||
};
|
||||
|
||||
export type OperationsWorkbenchItem = {
|
||||
id: string;
|
||||
tone: WorkbenchTone;
|
||||
title: string;
|
||||
detail: string;
|
||||
action_label: string;
|
||||
target: string;
|
||||
};
|
||||
|
||||
export type OperationsWorkbenchActivity = {
|
||||
occurred_at: string;
|
||||
actor: string;
|
||||
text: string;
|
||||
};
|
||||
|
||||
export type OperationsWorkbenchDto = {
|
||||
role: "admin" | "model_team" | "business_team";
|
||||
alerts: string[];
|
||||
kpis: OperationsWorkbenchKpi[];
|
||||
todos: OperationsWorkbenchItem[];
|
||||
watches: OperationsWorkbenchItem[];
|
||||
activities: OperationsWorkbenchActivity[];
|
||||
};
|
||||
|
||||
type ApiEnvelope<T> = {
|
||||
data: T;
|
||||
meta?: Record<string, unknown>;
|
||||
@@ -66,8 +99,7 @@ export class OperationsApiError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
const configuredMode = import.meta.env.VITE_OPERATIONS_API_MODE;
|
||||
export const operationsApiMode: OperationsApiMode = configuredMode === "api" ? "api" : "mock";
|
||||
export const operationsApiMode: OperationsApiMode = "api";
|
||||
const API_BASE = "/api/v1/operations";
|
||||
|
||||
function requireWorkspaceId(workspaceId?: string): string {
|
||||
@@ -117,28 +149,7 @@ function toModelRecord(dto: OperationsModelDto): ModelRecord {
|
||||
};
|
||||
}
|
||||
|
||||
function mockDelay(signal?: AbortSignal): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const timer = window.setTimeout(resolve, 160);
|
||||
signal?.addEventListener("abort", () => {
|
||||
window.clearTimeout(timer);
|
||||
reject(new DOMException("Request aborted", "AbortError"));
|
||||
}, { once: true });
|
||||
});
|
||||
}
|
||||
|
||||
export async function listOperationsModels(params: OperationsModelListParams = {}): Promise<ModelRecord[]> {
|
||||
if (operationsApiMode === "mock") {
|
||||
await mockDelay(params.signal);
|
||||
const keyword = params.keyword?.trim().toLowerCase();
|
||||
return MODELS
|
||||
.filter((model) => !params.bank || model.bank === params.bank)
|
||||
.filter((model) => !params.category || model.category === params.category)
|
||||
.filter((model) => !params.status || model.status === params.status)
|
||||
.filter((model) => !keyword || `${model.bank} ${model.name} ${model.modelId} ${model.version}`.toLowerCase().includes(keyword))
|
||||
.map((model) => ({ ...model }));
|
||||
}
|
||||
|
||||
const query = new URLSearchParams();
|
||||
query.set("workspace_id", requireWorkspaceId(params.workspaceId));
|
||||
if (params.bank) query.set("bank", params.bank);
|
||||
@@ -155,12 +166,6 @@ export async function getOperationsModel(
|
||||
workspaceId?: string,
|
||||
signal?: AbortSignal,
|
||||
): Promise<ModelRecord> {
|
||||
if (operationsApiMode === "mock") {
|
||||
await mockDelay(signal);
|
||||
const model = MODELS.find((item) => item.modelId === modelId);
|
||||
if (!model) throw new OperationsApiError("模型不存在", 404, "MODEL_NOT_FOUND");
|
||||
return { ...model };
|
||||
}
|
||||
const query = new URLSearchParams({ workspace_id: requireWorkspaceId(workspaceId) });
|
||||
const data = await request<OperationsModelDto>(`/models/${encodeURIComponent(modelId)}?${query.toString()}`, { signal });
|
||||
return toModelRecord(data);
|
||||
@@ -172,12 +177,6 @@ export async function getMonthlyMonitoringResult(
|
||||
workspaceId?: string,
|
||||
signal?: AbortSignal,
|
||||
): Promise<MonitoringRow> {
|
||||
if (operationsApiMode === "mock") {
|
||||
await mockDelay(signal);
|
||||
const result = monitoringRows(MODELS).find((item) => item.modelId === modelId && item.monitorMonth === month);
|
||||
if (!result) throw new OperationsApiError("该月份暂无监控结果", 404, "MONITOR_RESULT_NOT_FOUND");
|
||||
return { ...result };
|
||||
}
|
||||
const model = await getOperationsModel(modelId, workspaceId, signal);
|
||||
const query = new URLSearchParams({
|
||||
month,
|
||||
@@ -197,3 +196,11 @@ export async function getMonthlyMonitoringResult(
|
||||
secondaryHits: data.secondary_hits_6m,
|
||||
};
|
||||
}
|
||||
|
||||
export async function getOperationsWorkbench(
|
||||
workspaceId?: string,
|
||||
signal?: AbortSignal,
|
||||
): Promise<OperationsWorkbenchDto> {
|
||||
const query = new URLSearchParams({ workspace_id: requireWorkspaceId(workspaceId) });
|
||||
return request<OperationsWorkbenchDto>(`/workbench?${query.toString()}`, { signal });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user