239 lines
10 KiB
TypeScript
239 lines
10 KiB
TypeScript
/**
|
|
* 轻应用共享常量 + 运行时 URL 构造(管理页预览 / 列表页跳转 / iframe 宿主共用)。
|
|
*
|
|
* 登录参数运行时从 `userInfo`(JSON) 解析(经 authStorage:嵌入态 sessionStorage / 正常态
|
|
* localStorage,见 scoped-storage.ts);主题参数用配置值。
|
|
*/
|
|
|
|
import type { LightApp, LightAppRouteParam } from "@/strategy-components/api/light-apps";
|
|
import { getStoredAuth } from "@/core/auth";
|
|
import { authStorageGet } from "@/core/auth/scoped-storage";
|
|
import { readCurrentExternalAccessTokenValue } from "@/core/auth/external-access-token";
|
|
|
|
/** iframe 可挂载的父菜单(取自 core/page-layout/sidebar-menu.ts 的现有菜单 id)。 */
|
|
export const MOUNT_OPTIONS: { label: string; value: string }[] = [
|
|
{ label: "XK中任务", value: "xk-center-task" },
|
|
{ label: "COH中任务", value: "coh-center-task" },
|
|
{ label: "PG中任务", value: "pg-center-task" },
|
|
{ label: "TY中任务", value: "ty-center-task" },
|
|
{ label: "资源管理", value: "resource-management" },
|
|
];
|
|
|
|
export function mountLabel(value: string | null | undefined): string {
|
|
if (!value) return "";
|
|
return MOUNT_OPTIONS.find((o) => o.value === value)?.label ?? value;
|
|
}
|
|
|
|
/**
|
|
* 登录参数可选来源;值均可在 userInfo 中取到,"other" 表示自填。
|
|
* accessToken / yUserId 取登录路由透传值(写入 userInfo 的同名键);
|
|
* userName 优先取当前鉴权会话的原始登录名;gateDepartName / typeName 取 consumer
|
|
* 接口 userDetailByYuserId 的返回值。
|
|
*/
|
|
export const LOGIN_SOURCE_OPTIONS: { label: string; value: string }[] = [
|
|
{ label: "access_token", value: "access_token" },
|
|
{ label: "accessToken(兼容旧配置)", value: "accessToken" },
|
|
{ label: "yUserId", value: "yUserId" },
|
|
{ label: "userName(当前登录账号)", value: "userName" },
|
|
{ label: "gateDepartName", value: "gateDepartName" },
|
|
{ label: "typeName", value: "typeName" },
|
|
{ label: "其他(自定义值)", value: "other" },
|
|
];
|
|
|
|
/**
|
|
* 我方主题色键:red(红白)、green(WeKnora 绿色)、blue(浅蓝)、dark-blue(深蓝)。
|
|
* 主题参数的映射以这些键为左侧 key(如 `dark-blue=darkBlue`)。
|
|
*/
|
|
export type AppThemeColor = "red" | "green" | "blue" | "iceblue" | "lightyellow" | "dark-blue";
|
|
|
|
/** 我方全部主题色(「应用管理」主题参数逐项配置目标值时遍历;dot 为配色小圆点)。 */
|
|
export const APP_THEME_COLORS: { key: AppThemeColor; label: string; dot: string }[] = [
|
|
{ key: "green", label: "绿色", dot: "#07C05F" },
|
|
{ key: "red", label: "红色", dot: "#F5222D" },
|
|
{ key: "green", label: "绿色", dot: "#07C05F" },
|
|
{ key: "blue", label: "蓝色", dot: "#1677FF" },
|
|
{ key: "iceblue", label: "冰川蓝", dot: "#9BCEF1" },
|
|
{ key: "lightyellow", label: "浅黄色", dot: "#F3FBD9" },
|
|
{ key: "dark-blue", label: "深蓝色", dot: "#002B5C" },
|
|
];
|
|
|
|
/**
|
|
* 主题键别名:兼容历史/手填里用 light/dark 等写法的映射键,归一到当前主题色键。
|
|
* 查映射时按别名顺序尝试,避免「键对不上 → 回退到 red」的恒红 bug。
|
|
*/
|
|
const THEME_KEY_ALIASES: Record<AppThemeColor, string[]> = {
|
|
red: ["red", "light"],
|
|
green: ["green", "lightgreen", "light-green"],
|
|
blue: ["blue"],
|
|
iceblue: ["iceblue", "ice-blue"],
|
|
lightyellow: ["lightyellow", "light-yellow", "yellow"],
|
|
"dark-blue": ["dark-blue", "darkblue", "dark"],
|
|
};
|
|
|
|
/**
|
|
* 读取我方当前主题色,以 ThemeContext / next-themes 写在 <html> 上的 class 为准
|
|
* (含嵌入场景的强制主题)。深色=dark-blue,绿色=green,浅蓝=blue,其余(浅色/红白)=red。
|
|
*/
|
|
export function getCurrentTheme(): AppThemeColor {
|
|
if (typeof document !== "undefined") {
|
|
const classList = document.documentElement.classList;
|
|
if (classList.contains("dark")) return "dark-blue";
|
|
if (classList.contains("lightgreen")) return "green";
|
|
if (classList.contains("lightblue")) return "blue";
|
|
if (classList.contains("iceblue")) return "iceblue";
|
|
if (classList.contains("lightyellow")) return "lightyellow";
|
|
}
|
|
return "red";
|
|
}
|
|
|
|
/** 解析主题映射串(`red=red;dark-blue=darkBlue`)为 { 主题色键: 目标值 }。值不做 trim,便于表单回显。 */
|
|
export function parseThemeMap(raw: string | null | undefined): Record<string, string> {
|
|
const map: Record<string, string> = {};
|
|
for (const pair of (raw ?? "").split(/[;,]/)) {
|
|
const idx = pair.search(/[=:]/);
|
|
if (idx <= 0) continue;
|
|
const k = pair.slice(0, idx).trim().toLowerCase();
|
|
map[k] = pair.slice(idx + 1);
|
|
}
|
|
return map;
|
|
}
|
|
|
|
/** 把主题映射对象序列化为映射串,按 APP_THEME_COLORS 顺序输出,缺省走同名(identity)。 */
|
|
export function stringifyThemeMap(map: Record<string, string>): string {
|
|
return APP_THEME_COLORS.map((c) => `${c.key}=${map[c.key] ?? c.key}`).join(";");
|
|
}
|
|
|
|
/**
|
|
* 按我方当前主题,从配置中取目标轻应用对应的值(跳转 / iframe 嵌套时注入)。
|
|
*
|
|
* 配置支持两种写法(管理员在「应用管理」主题参数里配置):
|
|
* 1) 映射串:`red=red;blue=blue;dark-blue=darkBlue` —— 按当前主题(含别名)取对应值;
|
|
* 取不到时回退到主题色名本身(identity),而非盲目回退首个值(会恒为 red)。
|
|
* 2) 纯值:`red` —— 不区分主题,原样返回。
|
|
*/
|
|
export function resolveThemeParamValue(
|
|
raw: string | null | undefined,
|
|
theme: AppThemeColor = getCurrentTheme(),
|
|
): string {
|
|
const value = (raw ?? "").trim();
|
|
if (!value) return theme;
|
|
// 不含键值分隔符 → 纯值,不区分主题原样返回
|
|
if (!/[=:]/.test(value)) return value;
|
|
const map = parseThemeMap(value);
|
|
for (const key of THEME_KEY_ALIASES[theme] ?? [theme]) {
|
|
if (map[key] != null) return map[key].trim();
|
|
}
|
|
// 映射里没有当前主题(含别名)→ 退回主题色名本身,避免恒为 red
|
|
return theme;
|
|
}
|
|
|
|
/** 读取并解析 localStorage 中的 userInfo。 */
|
|
export function readUserInfo(): Record<string, unknown> {
|
|
try {
|
|
const raw = authStorageGet("userInfo");
|
|
if (!raw) return {};
|
|
const parsed = JSON.parse(raw);
|
|
return parsed && typeof parsed === "object" ? (parsed as Record<string, unknown>) : {};
|
|
} catch {
|
|
return {};
|
|
}
|
|
}
|
|
|
|
/** 解析单个路由参数在运行时的取值。 */
|
|
export function resolveRouteParamValue(
|
|
param: LightAppRouteParam,
|
|
userInfo: Record<string, unknown> = readUserInfo(),
|
|
theme?: AppThemeColor,
|
|
): string {
|
|
if (param.paramType === "theme") {
|
|
// 主题参数:按我方当前主题,从管理员配置的映射中取目标应用对应的值。
|
|
// theme 显式传入时优先(避免切主题瞬间 DOM 的 dark class 还没更新导致读旧值)。
|
|
return resolveThemeParamValue(param.themeValue, theme);
|
|
}
|
|
// login
|
|
if (param.source === "other") {
|
|
return param.customValue ?? "";
|
|
}
|
|
if (param.source) {
|
|
// `userInfo.userName` can belong to a prior external-login flow (for
|
|
// example a stale "guest" value). Route parameters must follow the user
|
|
// currently authenticated in DeerFlow, so prefer the backend's original
|
|
// login username and only fall back to userInfo for older backends.
|
|
if (param.source === "userName") {
|
|
const auth = getStoredAuth();
|
|
const loginUsername = auth?.username?.trim();
|
|
if (loginUsername) return loginUsername;
|
|
|
|
const accountName = (auth?.email ?? "").split("@")[0]?.trim() || auth?.user_id;
|
|
if (accountName) return accountName;
|
|
|
|
const userInfoName = userInfo.userName;
|
|
if (typeof userInfoName === "string" && userInfoName.trim()) {
|
|
return userInfoName.trim();
|
|
}
|
|
|
|
return "";
|
|
}
|
|
const value = userInfo[param.source];
|
|
if (param.source === "access_token" || param.source === "accessToken") {
|
|
return readCurrentExternalAccessTokenValue(userInfo);
|
|
}
|
|
return value == null ? "" : String(value);
|
|
}
|
|
return "";
|
|
}
|
|
|
|
/**
|
|
* 构造应用的最终访问 URL:在 `app.url` 上追加解析后的路由参数查询串。
|
|
*
|
|
* 兼容 hash 路由地址(如 `https://x#/route`)—— 管理员粘贴完整地址,参数自动追加在末尾,
|
|
* 落在 hash 路由的 query 上(与现有 XK/PG 页面 `?accessToken=` 拼接方式一致)。
|
|
*/
|
|
function appendQueryPairs(url: string, pairs: string[]): string {
|
|
if (pairs.length === 0) return url;
|
|
const hashIndex = url.indexOf("#");
|
|
if (hashIndex >= 0) {
|
|
const beforeHash = url.slice(0, hashIndex);
|
|
const hash = url.slice(hashIndex);
|
|
const sep = hash.includes("?") ? "&" : "?";
|
|
return `${beforeHash}${hash}${sep}${pairs.join("&")}`;
|
|
}
|
|
const sep = url.includes("?") ? "&" : "?";
|
|
return `${url}${sep}${pairs.join("&")}`;
|
|
}
|
|
|
|
function replaceAccessTokenParam(url: string, accessToken: string): { url: string; replaced: boolean } {
|
|
if (!accessToken) return { url, replaced: false };
|
|
let replaced = false;
|
|
const nextUrl = url.replace(/([?&]access_token)(?:=[^&#]*)?(?=[&#]|$)/g, (match) => {
|
|
replaced = true;
|
|
return `${match.split("=")[0]}=${encodeURIComponent(accessToken)}`;
|
|
});
|
|
return { url: nextUrl, replaced };
|
|
}
|
|
|
|
export function buildLightAppUrl(
|
|
app: Pick<LightApp, "url" | "routeParams">,
|
|
userInfo: Record<string, unknown> = readUserInfo(),
|
|
theme?: AppThemeColor,
|
|
): string {
|
|
const routeParams = app.routeParams ?? [];
|
|
const accessToken = readCurrentExternalAccessTokenValue(userInfo);
|
|
const tokenUrlResult = replaceAccessTokenParam(app.url, accessToken);
|
|
const pairs = routeParams
|
|
.filter((p) => (p.key ?? "").trim())
|
|
.filter((p) => (p.key ?? "").trim() !== "access_token" || !tokenUrlResult.replaced)
|
|
.map((p) => {
|
|
const key = (p.key ?? "").trim();
|
|
const value = key === "access_token" ? accessToken : resolveRouteParamValue(p, userInfo, theme);
|
|
return `${encodeURIComponent(key)}=${encodeURIComponent(value)}`;
|
|
});
|
|
const hasAccessTokenParam =
|
|
tokenUrlResult.replaced ||
|
|
routeParams.some((p) => (p.key ?? "").trim() === "access_token");
|
|
if (!hasAccessTokenParam && accessToken) {
|
|
pairs.push(`${encodeURIComponent("access_token")}=${encodeURIComponent(accessToken)}`);
|
|
}
|
|
return appendQueryPairs(tokenUrlResult.url, pairs);
|
|
}
|