/** * 轻应用共享常量 + 运行时 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 = { 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 写在 上的 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 { const map: Record = {}; 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 { 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 { try { const raw = authStorageGet("userInfo"); if (!raw) return {}; const parsed = JSON.parse(raw); return parsed && typeof parsed === "object" ? (parsed as Record) : {}; } catch { return {}; } } /** 解析单个路由参数在运行时的取值。 */ export function resolveRouteParamValue( param: LightAppRouteParam, userInfo: Record = 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, userInfo: Record = 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); }