feat: light app center and iframe refactor
This commit is contained in:
1 parent
728a4b1170
commit
9d73f27146
35 files changed
+1878
-246
No files matched your search
@@ -0,0 +1,228 @@
|
||||
/**
|
||||
* 轻应用中心(应用管理 / 应用列表)API 客户端 + React Query hooks。
|
||||
*
|
||||
* 后端:`/api/light-apps`(见 app/gateway/routers/light_apps.py)。
|
||||
* 全局共享、无用户隔离;读对所有登录用户开放,写仅 admin。
|
||||
* snake_case ⇄ camelCase 在此边界收敛;路由参数项本身用 camelCase
|
||||
* (key/paramType/source/customValue/themeValue),与后端 RouteParam 字段一致,原样透传。
|
||||
*/
|
||||
|
||||
import {
|
||||
keepPreviousData,
|
||||
useMutation,
|
||||
useQuery,
|
||||
useQueryClient,
|
||||
} from "@tanstack/react-query";
|
||||
|
||||
import { apiFetch } from "@/core/api/fetch-client";
|
||||
import { getBackendBaseURL } from "@/core/config";
|
||||
|
||||
export type LightAppType = "window_open" | "iframe";
|
||||
export type RouteParamType = "login" | "theme";
|
||||
|
||||
/** 路由参数项。登录参数运行时从 localStorage.userInfo 解析;主题参数用 themeValue。 */
|
||||
export interface LightAppRouteParam {
|
||||
key: string;
|
||||
paramType: RouteParamType;
|
||||
/** 登录参数:accessToken / yUserId / userName / "other"。 */
|
||||
source?: string | null;
|
||||
/** 登录参数且 source==="other" 时的字面值。 */
|
||||
customValue?: string | null;
|
||||
/** 主题参数的值(dark-blue / red / blue,可改)。 */
|
||||
themeValue?: string | null;
|
||||
}
|
||||
|
||||
export interface LightApp {
|
||||
appId: string;
|
||||
name: string;
|
||||
description: string | null;
|
||||
appType: LightAppType;
|
||||
url: string;
|
||||
routeParams: LightAppRouteParam[];
|
||||
/** iframe 挂载的父菜单 id(window_open 为 null)。 */
|
||||
mountLocation: string | null;
|
||||
icon: string | null;
|
||||
enabled: boolean;
|
||||
sortOrder: number;
|
||||
createdAt: string | null;
|
||||
updatedAt: string | null;
|
||||
}
|
||||
|
||||
export interface LightAppInput {
|
||||
name: string;
|
||||
description?: string | null;
|
||||
appType: LightAppType;
|
||||
url: string;
|
||||
routeParams: LightAppRouteParam[];
|
||||
mountLocation?: string | null;
|
||||
icon?: string | null;
|
||||
enabled?: boolean;
|
||||
sortOrder?: number;
|
||||
}
|
||||
|
||||
export interface ListLightAppsParams {
|
||||
appType?: LightAppType;
|
||||
mountLocation?: string;
|
||||
enabledOnly?: boolean;
|
||||
search?: string;
|
||||
}
|
||||
|
||||
const BASE = () => `${getBackendBaseURL()}/api/light-apps`;
|
||||
|
||||
interface ServerRouteParam {
|
||||
key: string;
|
||||
paramType: RouteParamType;
|
||||
source?: string | null;
|
||||
customValue?: string | null;
|
||||
themeValue?: string | null;
|
||||
}
|
||||
|
||||
interface ServerApp {
|
||||
app_id: string;
|
||||
name: string;
|
||||
description: string | null;
|
||||
app_type: LightAppType;
|
||||
url: string;
|
||||
route_params: ServerRouteParam[];
|
||||
mount_location: string | null;
|
||||
icon: string | null;
|
||||
enabled: boolean;
|
||||
sort_order: number;
|
||||
created_at: string | null;
|
||||
updated_at: string | null;
|
||||
}
|
||||
|
||||
function toApp(s: ServerApp): LightApp {
|
||||
return {
|
||||
appId: s.app_id,
|
||||
name: s.name,
|
||||
description: s.description ?? null,
|
||||
appType: s.app_type,
|
||||
url: s.url,
|
||||
routeParams: (s.route_params ?? []).map((p) => ({
|
||||
key: p.key,
|
||||
paramType: p.paramType,
|
||||
source: p.source ?? null,
|
||||
customValue: p.customValue ?? null,
|
||||
themeValue: p.themeValue ?? null,
|
||||
})),
|
||||
mountLocation: s.mount_location ?? null,
|
||||
icon: s.icon ?? null,
|
||||
enabled: s.enabled ?? true,
|
||||
sortOrder: s.sort_order ?? 0,
|
||||
createdAt: s.created_at ?? null,
|
||||
updatedAt: s.updated_at ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
function toBody(input: Partial<LightAppInput>): Record<string, unknown> {
|
||||
const body: Record<string, unknown> = {};
|
||||
if (input.name !== undefined) body.name = input.name;
|
||||
if (input.description !== undefined) body.description = input.description;
|
||||
if (input.appType !== undefined) body.app_type = input.appType;
|
||||
if (input.url !== undefined) body.url = input.url;
|
||||
if (input.routeParams !== undefined) body.route_params = input.routeParams;
|
||||
if (input.mountLocation !== undefined) body.mount_location = input.mountLocation;
|
||||
if (input.icon !== undefined) body.icon = input.icon;
|
||||
if (input.enabled !== undefined) body.enabled = input.enabled;
|
||||
if (input.sortOrder !== undefined) body.sort_order = input.sortOrder;
|
||||
return body;
|
||||
}
|
||||
|
||||
async function readJson<T>(res: Response, ctx: string): Promise<T> {
|
||||
if (!res.ok) {
|
||||
const data = await res.json().catch(() => null as unknown);
|
||||
const detail =
|
||||
data && typeof data === "object" && "detail" in data
|
||||
? String((data as { detail: unknown }).detail)
|
||||
: `${ctx} failed (${res.status})`;
|
||||
throw new Error(detail);
|
||||
}
|
||||
return (await res.json()) as T;
|
||||
}
|
||||
|
||||
export async function listLightApps(params: ListLightAppsParams = {}): Promise<LightApp[]> {
|
||||
const qs = new URLSearchParams();
|
||||
if (params.appType) qs.set("app_type", params.appType);
|
||||
if (params.mountLocation) qs.set("mount_location", params.mountLocation);
|
||||
if (params.enabledOnly) qs.set("enabled_only", "true");
|
||||
if (params.search) qs.set("search", params.search);
|
||||
const query = qs.toString();
|
||||
const res = await apiFetch(`${BASE()}${query ? `?${query}` : ""}`, { method: "GET" });
|
||||
const data = await readJson<{ apps: ServerApp[] }>(res, "list light apps");
|
||||
return (data.apps ?? []).map(toApp);
|
||||
}
|
||||
|
||||
export async function getLightApp(appId: string): Promise<LightApp> {
|
||||
const res = await apiFetch(`${BASE()}/${encodeURIComponent(appId)}`, { method: "GET" });
|
||||
return toApp(await readJson<ServerApp>(res, "get light app"));
|
||||
}
|
||||
|
||||
export async function createLightApp(input: LightAppInput): Promise<LightApp> {
|
||||
const res = await apiFetch(BASE(), {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(toBody(input)),
|
||||
});
|
||||
return toApp(await readJson<ServerApp>(res, "create light app"));
|
||||
}
|
||||
|
||||
export async function updateLightApp(
|
||||
appId: string,
|
||||
patch: Partial<LightAppInput>,
|
||||
): Promise<LightApp> {
|
||||
const res = await apiFetch(`${BASE()}/${encodeURIComponent(appId)}`, {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(toBody(patch)),
|
||||
});
|
||||
return toApp(await readJson<ServerApp>(res, "update light app"));
|
||||
}
|
||||
|
||||
export async function deleteLightApp(appId: string): Promise<void> {
|
||||
const res = await apiFetch(`${BASE()}/${encodeURIComponent(appId)}`, { method: "DELETE" });
|
||||
await readJson<{ success: boolean }>(res, "delete light app");
|
||||
}
|
||||
|
||||
// ── React Query hooks(管理页 / 列表页 / 侧边栏共用缓存) ───────────────────
|
||||
|
||||
const LIGHT_APPS_KEY = "light-apps";
|
||||
|
||||
export function useLightApps(params: ListLightAppsParams = {}) {
|
||||
return useQuery({
|
||||
queryKey: [
|
||||
LIGHT_APPS_KEY,
|
||||
"list",
|
||||
params.appType ?? "",
|
||||
params.mountLocation ?? "",
|
||||
params.enabledOnly ?? false,
|
||||
params.search ?? "",
|
||||
],
|
||||
queryFn: () => listLightApps(params),
|
||||
placeholderData: keepPreviousData,
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
}
|
||||
|
||||
export function useLightApp(appId: string | undefined) {
|
||||
return useQuery({
|
||||
queryKey: [LIGHT_APPS_KEY, "detail", appId ?? ""],
|
||||
queryFn: () => getLightApp(appId as string),
|
||||
enabled: !!appId,
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
}
|
||||
|
||||
export function useLightAppActions() {
|
||||
const queryClient = useQueryClient();
|
||||
const invalidate = () => queryClient.invalidateQueries({ queryKey: [LIGHT_APPS_KEY] });
|
||||
return {
|
||||
create: useMutation({ mutationFn: createLightApp, onSuccess: invalidate }),
|
||||
update: useMutation({
|
||||
mutationFn: ({ appId, patch }: { appId: string; patch: Partial<LightAppInput> }) =>
|
||||
updateLightApp(appId, patch),
|
||||
onSuccess: invalidate,
|
||||
}),
|
||||
remove: useMutation({ mutationFn: deleteLightApp, onSuccess: invalidate }),
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user