Files
YschcmsFront/src/utils/themeParamForm.ts
T
2026-09-07 09:11:37 +08:00

124 lines
5.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { ThemeParamVo, ThemeVo } from "@/types/cms";
/**
* themeParams 纯函数模块(方案 §6.3,评审 R10/R6):schema → 表单模型映射、
* 值校验(color 正则 / select 枚举 / image 仅 /upload/**)、themeParams ↔ configJson 装拆。
* 与后端合并语义(方案 §5.3)保持一致:仅替换 theme_params 键、保留其余键、丢弃未声明键。
* vitest 单测对象(§8.4 T28-T30),不依赖组件挂载基建。
*/
/** color 合法值:#RGB/#RRGGBB/#RRGGBBAA(与后端 ThemeParam.COLOR_PATTERN 一致) */
export const COLOR_RE = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/;
/** param key 约束(与后端 KEY_PATTERN 一致) */
export const KEY_RE = /^[a-zA-Z][a-zA-Z0-9_]{0,31}$/;
export interface FormModelValue {
[key: string]: string;
}
/** 单值校验(与后端 ThemeParam.isValidValue 同语义;null/空一律不合法) */
export function isValidParamValue(param: ThemeParamVo, value: string | undefined | null): boolean {
if (value === undefined || value === null || value === "") return false;
switch (param.type) {
case "color":
return COLOR_RE.test(value);
case "image":
// B-7:与后端对齐——附件 webUrl 相对形态含 context-path(/cms/upload/**)
return value.startsWith("/cms/upload/");
case "select":
return !!param.options?.some(o => o.value === value);
case "text":
return value.length <= 256;
default:
return false;
}
}
/** 取参数生效值:显式值合法用之;否则回退 defaultValue(缺省也视为合法默认) */
export function effectiveValue(param: ThemeParamVo, model: FormModelValue): string {
const raw = model[param.key];
if (isValidParamValue(param, raw)) return raw as string;
return param.defaultValue ?? "";
}
/** T28: schema → 表单模型(声明过的 key 全部带上生效值) */
export function formModelFromTheme(theme: ThemeVo, existing: FormModelValue = {}): FormModelValue {
const model: FormModelValue = {};
theme.params.forEach(p => {
const raw = existing[p.key];
model[p.key] = isValidParamValue(p, raw) ? (raw as string) : (p.defaultValue ?? "");
});
return model;
}
/** T28: 值校验兜底 —— 收集当前表单模型里的非法值 key(空值不报,默认值兜底) */
export function invalidParamKeys(theme: ThemeVo, model: FormModelValue): string[] {
const result: string[] = [];
theme.params.forEach(p => {
const v = model[p.key];
if (v !== undefined && v !== "" && !isValidParamValue(p, v)) {
result.push(p.key);
}
});
return result;
}
/** T30a: 服务端合并语义(与后端 §5.3 一致)—— 仅替换 theme_params、保留其余键、丢弃未声明/非法值 */
export function mergeThemeParamsIntoConfigJson(configJson: string | undefined | null,
theme: ThemeVo,
model: FormModelValue): string {
let config: Record<string, unknown> = {};
if (configJson && configJson.trim()) {
try {
const parsed = JSON.parse(configJson);
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
config = { ...parsed };
}
} catch {
// 非合法 JSON 按空配置处理(与后端 ExtJsonUtils.parse 语义一致)
}
}
const filtered: Record<string, string> = {};
theme.params.forEach(p => {
const v = model[p.key];
if (isValidParamValue(p, v)) {
filtered[p.key] = v as string;
}
});
if (Object.keys(filtered).length === 0) {
delete config.theme_params;
} else {
config.theme_params = filtered;
}
return JSON.stringify(config);
}
/** T30b: configJson → themeParams(编辑回显:仅取声明过的 key,非法值丢弃) */
export function themeParamsFromConfigJson(configJson: string | undefined | null,
theme: ThemeVo): FormModelValue {
const model: FormModelValue = {};
if (!configJson) return model;
try {
const parsed = JSON.parse(configJson);
const tp = parsed && typeof parsed === "object" && !Array.isArray(parsed)
? (parsed as Record<string, unknown>).theme_params
: undefined;
if (tp && typeof tp === "object") {
theme.params.forEach(p => {
const raw = (tp as Record<string, unknown>)[p.key];
const v = typeof raw === "string" ? raw : "";
if (isValidParamValue(p, v)) model[p.key] = v;
});
}
} catch {
// 非合法 JSON 按无参数处理
}
return model;
}
/** 主题是否声明了某类型参数(组件渲染分支用) */
export function hasParamType(theme: ThemeVo | undefined | null, type: string): boolean {
return !!theme?.params?.some(p => p.type === type);
}