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 = {}; 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 = {}; 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).theme_params : undefined; if (tp && typeof tp === "object") { theme.params.forEach(p => { const raw = (tp as Record)[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); }