feat: light app center and iframe refactor

This commit is contained in:
fengxiangdebaozi committed 2026-06-12 15:40:09 +08:00
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 }),
};
}