feat(workbench): integrate runtime data and map tooling

This commit is contained in:
2026-09-22 10:45:46 +08:00
parent bf44cc0f25
commit bebcf2a06c
163 changed files with 15642 additions and 1312 deletions
@@ -0,0 +1,196 @@
import { z } from "zod";
import type { AccessTokenProvider } from "@/shared/auth/keycloak-auth";
import { env } from "@/shared/config/env";
import type { NetworkViewParams } from "../map/camera";
import { GEOSERVER_WORKSPACE } from "../map/geoserver-config";
const projectSchema = z.object({
project_id: z.string().min(1),
name: z.string().trim().min(1).optional(),
code: z.string().trim().min(1).optional(),
gs_workspace: z.string().min(1),
status: z.string(),
project_role: z.string().trim().min(1).optional()
});
const projectPageSchema = z.object({
items: z.array(projectSchema),
total: z.number().int().nonnegative(),
limit: z.number().int().positive(),
offset: z.number().int().nonnegative()
});
export type WorkbenchProject = z.infer<typeof projectSchema>;
const projectMetadataSchema = z.object({
project_id: z.string().min(1),
name: z.string().trim().min(1),
code: z.string().trim().min(1),
gs_workspace: z.string().trim().min(1),
map_extent: z.unknown().nullable().optional(),
status: z.string(),
project_role: z.string().trim().min(1)
});
const accessContextSchema = z.object({
user_id: z.string().min(1),
username: z.string().trim().min(1),
system_role: z.string().trim().min(1),
is_system_admin: z.boolean(),
project_id: z.string().min(1).nullable().optional(),
project_role: z.string().trim().min(1).nullable().optional(),
permissions: z.array(z.string())
});
const bboxSchema = z.tuple([
z.number().finite(),
z.number().finite(),
z.number().finite(),
z.number().finite()
]);
const keyedExtentSchema = z.object({
xmin: z.number().finite(),
ymin: z.number().finite(),
xmax: z.number().finite(),
ymax: z.number().finite()
});
export type WorkbenchProjectPresentation = {
projectId: string;
projectName: string | null;
username: string | null;
projectRole: string | null;
systemRole: string | null;
isSystemAdmin: boolean;
networkView: NetworkViewParams | null;
metadataAvailable: boolean;
accessContextAvailable: boolean;
};
const SERVER_API_BASE_URL = env.TJWATER_SERVER_API_BASE_URL.replace(/\/$/, "");
export async function findOwnedWorkbenchProject(
getAccessToken?: AccessTokenProvider,
signal?: AbortSignal
): Promise<{ project: WorkbenchProject; accessToken: string | null | undefined }> {
const accessToken = await getAccessToken?.();
const response = await fetch(`${SERVER_API_BASE_URL}/api/v1/projects?limit=1000&offset=0`, {
headers: createWorkbenchHeaders(accessToken),
signal
});
const payload: unknown = await response.json().catch(() => null);
if (!response.ok) throw new Error(getServerError(payload, response.status, "项目查询"));
const projects = projectPageSchema.parse(payload);
const project = projects.items.find(
(item) => item.status === "active" && item.gs_workspace === GEOSERVER_WORKSPACE
);
if (!project) throw new Error(`未找到GeoServer工作空间 ${GEOSERVER_WORKSPACE} 对应的授权项目`);
return { project, accessToken };
}
export async function fetchWorkbenchProjectPresentation(
getAccessToken?: AccessTokenProvider,
signal?: AbortSignal
): Promise<WorkbenchProjectPresentation> {
const { project, accessToken } = await findOwnedWorkbenchProject(getAccessToken, signal);
const headers = createWorkbenchHeaders(accessToken, project.project_id);
const [metadataResult, accessResult] = await Promise.allSettled([
fetchProjectMetadata(project, headers, signal),
fetchAccessContext(project, headers, signal)
]);
const metadata = metadataResult.status === "fulfilled" ? metadataResult.value : null;
const accessContext = accessResult.status === "fulfilled" ? accessResult.value : null;
return {
projectId: project.project_id,
projectName: metadata?.name ?? project.name ?? null,
username: accessContext?.username ?? null,
projectRole:
accessContext?.project_role
?? metadata?.project_role
?? project.project_role
?? null,
systemRole: accessContext?.system_role ?? null,
isSystemAdmin: accessContext?.is_system_admin ?? false,
networkView: parseProjectMapExtent(metadata?.map_extent),
metadataAvailable: metadata !== null,
accessContextAvailable: accessContext !== null
};
}
export function parseProjectMapExtent(value: unknown): NetworkViewParams | null {
let bbox: z.infer<typeof bboxSchema> | null = null;
if (value && typeof value === "object" && "bbox" in value) {
const parsed = bboxSchema.safeParse(value.bbox);
if (parsed.success) bbox = parsed.data;
} else {
const parsed = keyedExtentSchema.safeParse(value);
if (parsed.success) {
bbox = [parsed.data.xmin, parsed.data.ymin, parsed.data.xmax, parsed.data.ymax];
}
}
if (!bbox || bbox[0] >= bbox[2] || bbox[1] >= bbox[3]) return null;
const webMercatorLimit = 20_037_508.35;
if (bbox.some((coordinate) => Math.abs(coordinate) > webMercatorLimit)) return null;
return { bbox3857: bbox };
}
async function fetchProjectMetadata(
project: WorkbenchProject,
headers: Headers,
signal?: AbortSignal
) {
const response = await fetch(`${SERVER_API_BASE_URL}/api/v1/projects/current/metadata`, {
headers,
signal
});
const payload: unknown = await response.json().catch(() => null);
if (!response.ok) {
throw new Error(getServerError(payload, response.status, "项目元数据查询"));
}
const metadata = projectMetadataSchema.parse(payload);
if (metadata.project_id !== project.project_id) {
throw new Error("项目元数据与授权项目不一致");
}
return metadata;
}
async function fetchAccessContext(
project: WorkbenchProject,
headers: Headers,
signal?: AbortSignal
) {
const response = await fetch(`${SERVER_API_BASE_URL}/api/v1/access-context`, {
headers,
signal
});
const payload: unknown = await response.json().catch(() => null);
if (!response.ok) {
throw new Error(getServerError(payload, response.status, "访问上下文查询"));
}
const accessContext = accessContextSchema.parse(payload);
if (accessContext.project_id !== project.project_id) {
throw new Error("访问上下文与授权项目不一致");
}
return accessContext;
}
export function createWorkbenchHeaders(
accessToken: string | null | undefined,
projectId?: string,
json = false
) {
const headers = new Headers({ Accept: "application/json" });
if (json) headers.set("Content-Type", "application/json");
if (accessToken) headers.set("Authorization", `Bearer ${accessToken}`);
if (projectId) headers.set("X-Project-Id", projectId);
return headers;
}
export function getServerError(payload: unknown, status: number, operation: string) {
const detail = payload && typeof payload === "object" && "detail" in payload
? String(payload.detail)
: `HTTP ${status}`;
return `${operation}失败:${detail}`;
}