- 主题参数改为按系统主题(red/dark-blue)映射取目标应用对应的值 - 应用管理弹窗用结构化映射编辑器(逐主题色配置目标值,默认同名) - iframe 嵌套显式传入当前主题色,切主题时 src 重建 - 读取/保存时规范化映射串;解析支持 light/dark 等别名,取不到回退主题色名而非恒为 red Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
235 lines
7.7 KiB
TypeScript
235 lines
7.7 KiB
TypeScript
/**
|
||
* 轻应用中心(应用管理 / 应用列表)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";
|
||
import { parseThemeMap, stringifyThemeMap } from "@/strategy-components/lib/light-app";
|
||
|
||
export type LightAppType = "window_open" | "iframe";
|
||
export type RouteParamType = "login" | "theme";
|
||
|
||
/** 路由参数项。登录参数运行时从 localStorage.userInfo 解析;主题参数随当前系统主题自动注入。 */
|
||
export interface LightAppRouteParam {
|
||
key: string;
|
||
paramType: RouteParamType;
|
||
/** 登录参数:accessToken / yUserId / userName / "other"。 */
|
||
source?: string | null;
|
||
/** 登录参数且 source==="other" 时的字面值。 */
|
||
customValue?: string | null;
|
||
/** 主题参数配置:映射串 `light=red;dark=darkBlue`(或纯值);运行时按当前系统主题取对应值(见 resolveThemeParamValue)。 */
|
||
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,
|
||
// 主题参数读取时规范化为完整映射串(red=..;blue=..;dark-blue=..),
|
||
// 让旧的纯值/部分映射也能随主题切换,无需重新保存。
|
||
themeValue:
|
||
p.paramType === "theme"
|
||
? stringifyThemeMap(parseThemeMap(p.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 }),
|
||
};
|
||
}
|