主题色修改

This commit is contained in:
fengxiangdebaozi committed 2026-06-12 21:38:38 +08:00
1 parent 1214ea0d91
commit 00d6bd8595
4 files changed
+157 -61

No files matched your search

@@ -17,7 +17,8 @@ export default function LightAppIframeView() {
const isDark = theme === "dark"; const isDark = theme === "dark";
const { data: app, isLoading, error } = useLightApp(appId); const { data: app, isLoading, error } = useLightApp(appId);
const src = useMemo(() => (app ? buildLightAppUrl(app) : ""), [app]); // 依赖 theme:切换主题色时重建 src,把对应主题色重新传给被嵌套应用。
const src = useMemo(() => (app ? buildLightAppUrl(app) : ""), [app, theme]);
const centerClass = `flex h-full w-full items-center justify-center text-sm ${ const centerClass = `flex h-full w-full items-center justify-center text-sm ${
isDark ? "text-slate-400" : "text-slate-500" isDark ? "text-slate-400" : "text-slate-500"
@@ -23,9 +23,12 @@ import {
type LightAppType, type LightAppType,
} from "@/strategy-components/api/light-apps"; } from "@/strategy-components/api/light-apps";
import { import {
APP_THEME_COLORS,
LOGIN_SOURCE_OPTIONS, LOGIN_SOURCE_OPTIONS,
MOUNT_OPTIONS, MOUNT_OPTIONS,
mountLabel, mountLabel,
parseThemeMap,
stringifyThemeMap,
} from "@/strategy-components/lib/light-app"; } from "@/strategy-components/lib/light-app";
interface FormState { interface FormState {
@@ -53,7 +56,7 @@ const newParam = (): LightAppRouteParam => ({
paramType: "login", paramType: "login",
source: "accessToken", source: "accessToken",
customValue: "", customValue: "",
themeValue: "dark-blue", themeValue: "red=red;blue=blue;dark-blue=dark-blue",
}); });
function formFromApp(app: LightApp): FormState { function formFromApp(app: LightApp): FormState {
@@ -69,7 +72,7 @@ function formFromApp(app: LightApp): FormState {
paramType: p.paramType, paramType: p.paramType,
source: p.source ?? "accessToken", source: p.source ?? "accessToken",
customValue: p.customValue ?? "", customValue: p.customValue ?? "",
themeValue: p.themeValue ?? "dark-blue", themeValue: p.themeValue ?? "",
})), })),
}; };
} }
@@ -512,10 +515,12 @@ export default function LightAppManagementPage() {
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<span className="flex items-center text-[14px] font-medium"> <span className="flex items-center text-[14px] font-medium">
<span>路由参数</span> <span>路由参数</span>
<HelpCircle <span
className="ml-1 h-3.5 w-3.5 text-[#A6A6A6]" className="ml-1 inline-flex"
title="参数会在加载此应用时作为 query 参数拼接在 URL 的尾部" title="参数会在加载此应用时作为 query 参数拼接在 URL 的尾部"
/> >
<HelpCircle className="h-3.5 w-3.5 text-[#A6A6A6]" />
</span>
</span> </span>
<button <button
type="button" type="button"
@@ -529,41 +534,44 @@ export default function LightAppManagementPage() {
{form.routeParams.length > 0 ? ( {form.routeParams.length > 0 ? (
<div className="max-h-[220px] space-y-2 overflow-y-auto rounded-sm border border-[#E8E8E8] bg-[#FAFAFA] p-1"> <div className="max-h-[220px] space-y-2 overflow-y-auto rounded-sm border border-[#E8E8E8] bg-[#FAFAFA] p-1">
{form.routeParams.map((p, idx) => ( {form.routeParams.map((p, idx) => {
const themeMap =
p.paramType === "theme" ? parseThemeMap(p.themeValue) : {};
return (
<div <div
key={`${p.key}-${idx}`} key={idx}
className="relative flex items-center space-x-2 rounded-sm border border-[#E8E8E8] bg-white p-2" className="relative rounded-sm border border-[#E8E8E8] bg-white p-2"
> >
<div className="flex-1 space-y-1"> <div className="flex items-center space-x-2">
<input <div className="flex-1">
type="text" <input
value={p.key} type="text"
onChange={(e) => patchParam(idx, { key: e.target.value })} value={p.key}
placeholder="参数名 key" onChange={(e) => patchParam(idx, { key: e.target.value })}
className="h-8 w-full rounded-[2px] border border-[#DBDBDB] bg-white px-2 font-mono text-[12px] focus:border-[#F5222D] focus:outline-none focus:ring-px focus:ring-[#F5222D]" placeholder="参数名 key"
/> className="h-8 w-full rounded-[2px] border border-[#DBDBDB] bg-white px-2 font-mono text-[12px] focus:border-[#F5222D] focus:outline-none focus:ring-px focus:ring-[#F5222D]"
</div> />
</div>
<div className="w-[110px]"> <div className="w-[110px]">
<select <select
value={p.paramType} value={p.paramType}
onChange={(e) => onChange={(e) =>
patchParam(idx, { patchParam(idx, {
paramType: e.target.value as "login" | "theme", paramType: e.target.value as "login" | "theme",
source: source:
e.target.value === "login" ? (p.source || "accessToken") : undefined, e.target.value === "login" ? (p.source || "accessToken") : undefined,
}) })
} }
className="h-8 w-full rounded-[2px] border border-[#DBDBDB] bg-[#FAFAFA] px-1 text-[12px] focus:border-[#F5222D] focus:outline-none" className="h-8 w-full rounded-[2px] border border-[#DBDBDB] bg-[#FAFAFA] px-1 text-[12px] focus:border-[#F5222D] focus:outline-none"
> >
<option value="login">登录参数</option> <option value="login">登录参数</option>
<option value="theme">主题参数</option> <option value="theme">主题参数</option>
</select> </select>
</div> </div>
<div className="flex-[1.2] text-[11px]"> {p.paramType === "login" && (
{p.paramType === "login" ? ( <div className="flex flex-[1.2] items-center gap-2 text-[11px]">
<div className="flex items-center gap-2">
<select <select
value={p.source || "accessToken"} value={p.source || "accessToken"}
onChange={(e) => patchParam(idx, { source: e.target.value })} onChange={(e) => patchParam(idx, { source: e.target.value })}
@@ -587,26 +595,56 @@ export default function LightAppManagementPage() {
/> />
)} )}
</div> </div>
) : (
<input
type="text"
value={p.themeValue || ""}
onChange={(e) => patchParam(idx, { themeValue: e.target.value })}
placeholder="主题值,如 dark-blue"
className="h-8 w-full rounded-[2px] border border-[#DBDBDB] bg-white px-2 text-[12px] focus:border-[#F5222D] focus:outline-none focus:ring-px focus:ring-[#F5222D]"
/>
)} )}
<button
type="button"
onClick={() => removeParam(idx)}
className="cursor-pointer rounded-sm p-1 text-[#A6A6A6] transition-colors hover:bg-[#FEEDED] hover:text-[#F5222D]"
>
<X className="h-4 w-4" />
</button>
</div> </div>
<button {/* 主题参数:逐主题色配置传给目标应用的值(左=系统主题色,右=目标应用值,可改) */}
type="button" {p.paramType === "theme" && (
onClick={() => removeParam(idx)} <div className="mt-2 space-y-1.5 rounded-sm bg-[#FAFAFA] p-2">
className="cursor-pointer rounded-sm p-1 text-[#A6A6A6] transition-colors hover:bg-[#FEEDED] hover:text-[#F5222D]" <p className="text-[11px] leading-relaxed text-[#A6A6A6]">
> 各系统主题色跳转/嵌套时传给该应用的值(默认同名,可按对方命名修改)
<X className="h-4 w-4" /> </p>
</button> {APP_THEME_COLORS.map((c) => (
<div key={c.key} className="flex items-center gap-2">
<span className="flex w-[72px] shrink-0 items-center gap-1.5 text-[12px] text-[#575757]">
<span
className="h-2.5 w-2.5 shrink-0 rounded-full"
style={{ backgroundColor: c.dot }}
/>
{c.label}
</span>
<span className="shrink-0 font-mono text-[11px] text-[#A6A6A6]">
{c.key} =
</span>
<input
type="text"
value={themeMap[c.key] ?? c.key}
onChange={(e) =>
patchParam(idx, {
themeValue: stringifyThemeMap({
...themeMap,
[c.key]: e.target.value,
}),
})
}
placeholder={c.key}
className="h-7 flex-1 rounded-[2px] border border-[#DBDBDB] bg-white px-2 font-mono text-[12px] focus:border-[#F5222D] focus:outline-none focus:ring-px focus:ring-[#F5222D]"
/>
</div>
))}
</div>
)}
</div> </div>
))} );
})}
</div> </div>
) : ( ) : (
<div className="rounded-sm border border-dashed border-[#E8E8E8] bg-[#FAFAFA] p-4 text-center text-[12px] text-[#A6A6A6]"> <div className="rounded-sm border border-dashed border-[#E8E8E8] bg-[#FAFAFA] p-4 text-center text-[12px] text-[#A6A6A6]">
@@ -669,6 +707,7 @@ function cleanParams(params: LightAppRouteParam[]): LightAppRouteParam[] {
base.customValue = (p.customValue ?? "").trim(); base.customValue = (p.customValue ?? "").trim();
} }
} else { } else {
// theme:存映射串(或纯值),运行时按当前主题解析取值
base.themeValue = (p.themeValue ?? "").trim(); base.themeValue = (p.themeValue ?? "").trim();
} }
return base; return base;
@@ -20,7 +20,7 @@ import { getBackendBaseURL } from "@/core/config";
export type LightAppType = "window_open" | "iframe"; export type LightAppType = "window_open" | "iframe";
export type RouteParamType = "login" | "theme"; export type RouteParamType = "login" | "theme";
/** 路由参数项。登录参数运行时从 localStorage.userInfo 解析;主题参数用 themeValue。 */ /** 路由参数项。登录参数运行时从 localStorage.userInfo 解析;主题参数随当前系统主题自动注入。 */
export interface LightAppRouteParam { export interface LightAppRouteParam {
key: string; key: string;
paramType: RouteParamType; paramType: RouteParamType;
@@ -28,7 +28,7 @@ export interface LightAppRouteParam {
source?: string | null; source?: string | null;
/** 登录参数且 source==="other" 时的字面值。 */ /** 登录参数且 source==="other" 时的字面值。 */
customValue?: string | null; customValue?: string | null;
/** 主题参数的值(dark-blue / red / blue,可改)。 */ /** 主题参数配置:映射串 `light=red;dark=darkBlue`(或纯值);运行时按当前系统主题取对应值(见 resolveThemeParamValue)。 */
themeValue?: string | null; themeValue?: string | null;
} }
@@ -27,13 +27,68 @@ export const LOGIN_SOURCE_OPTIONS: { label: string; value: string }[] = [
{ label: "其他(自定义值)", value: "other" }, { label: "其他(自定义值)", value: "other" },
]; ];
/** 主题参数示例值(深蓝 / 红 / 蓝),可改。 */ /**
export const THEME_EXAMPLE_OPTIONS: { label: string; value: string }[] = [ * 我方主题色键。当前启用 red(浅色) / dark-blue(深色);blue 为后续将新增的主题。
{ label: "深蓝色 dark-blue", value: "dark-blue" }, * 主题参数的映射以这些键为左侧 key(如 `dark-blue=darkBlue`)。
{ label: "红色 red", value: "red" }, */
{ label: "蓝色 blue", value: "blue" }, export type AppThemeColor = "red" | "blue" | "dark-blue";
/** 我方全部主题色(「应用管理」主题参数逐项配置目标值时遍历;dot 为配色小圆点)。 */
export const APP_THEME_COLORS: { key: AppThemeColor; label: string; dot: string }[] = [
{ key: "red", label: "红色", dot: "#F5222D" },
{ key: "blue", label: "蓝色", dot: "#1677FF" },
{ key: "dark-blue", label: "深蓝色", dot: "#002B5C" },
]; ];
/**
* 读取我方当前主题色,以 ThemeContext 写在 <html> 上的 `dark` class 为准
* (含嵌入场景的强制主题)。当前:深色=dark-blue,浅色=red。
* TODO: 后续新增 blue 主题后,在此区分 blue 与 dark-blue。
*/
export function getCurrentTheme(): AppThemeColor {
if (typeof document !== "undefined" && document.documentElement.classList.contains("dark")) {
return "dark-blue";
}
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` —— 按当前主题取对应值;
* 缺当前主题映射时回退到 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);
return (map[theme] ?? map.red ?? Object.values(map)[0] ?? theme).trim();
}
/** 读取并解析 localStorage 中的 userInfo。 */ /** 读取并解析 localStorage 中的 userInfo。 */
export function readUserInfo(): Record<string, unknown> { export function readUserInfo(): Record<string, unknown> {
try { try {
@@ -52,7 +107,8 @@ export function resolveRouteParamValue(
userInfo: Record<string, unknown> = readUserInfo(), userInfo: Record<string, unknown> = readUserInfo(),
): string { ): string {
if (param.paramType === "theme") { if (param.paramType === "theme") {
return param.themeValue ?? ""; // 主题参数:按我方当前系统主题,从管理员配置的映射中取目标应用对应的值。
return resolveThemeParamValue(param.themeValue);
} }
// login // login
if (param.source === "other") { if (param.source === "other") {