init local code

This commit is contained in:
chenxing
2026-09-07 09:11:37 +08:00
commit a3a75c3680
112 changed files with 24837 additions and 0 deletions
+123
View File
@@ -0,0 +1,123 @@
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);
}