/** * 系统配置页纯函数:分组、草稿校验、命名模板预览与展示格式化。 * * R-26:校验与保存共用 shared 的 `validateSystemConfigValue` / `validateNamingTemplate`, * 页面不再维护第二套阈值 / 模板规则。 */ import { type NamingContext, type NamingTemplateCheck, resolveNamingTemplate, SYSTEM_CONFIG_GROUPS, type SystemConfig, systemConfigDefinition, type SystemConfigGroupKey, type SystemConfigValue, validateNamingTemplate, validateSystemConfigValue, } from '@lingxi/shared'; export interface ConfigGroupView { key: SystemConfigGroupKey; name: string; description: string; items: SystemConfig[]; } function itemSortOrder(config: SystemConfig): number { return systemConfigDefinition(config.key)?.sortOrder ?? 9_999; } /** 按 6 个固定分组渲染,组内按配置定义排序;空分组保留(页面展示空态) */ export function configGroupViews(configs: readonly SystemConfig[]): ConfigGroupView[] { return [...SYSTEM_CONFIG_GROUPS] .sort((left, right) => left.sortOrder - right.sortOrder) .map((group) => ({ key: group.key, name: group.name, description: group.description, items: configs .filter((config) => config.group === group.key) .sort((left, right) => itemSortOrder(left) - itemSortOrder(right)), })); } export function configKeyLabel(key: string): string { return systemConfigDefinition(key)?.label ?? key; } /** 原始值转输入框字符串(布尔用 'true' / 'false') */ export function rawValueOf(config: SystemConfig): string { if (typeof config.value === 'boolean') return config.value ? 'true' : 'false'; return String(config.value); } /** 输入框字符串按类型解析;非法输入返回 null(不抛错,用于行内校验) */ export function parseConfigRaw(config: SystemConfig, raw: string): SystemConfigValue | null { if (config.type === 'boolean') { if (raw === 'true') return true; if (raw === 'false') return false; return null; } if (config.type === 'number') { const trimmed = raw.trim(); if (trimmed === '') return null; const value = Number(trimmed); return Number.isFinite(value) ? value : null; } return raw; } export function isConfigDirty(config: SystemConfig, raw: string): boolean { const parsed = parseConfigRaw(config, raw); if (parsed === null) return true; return parsed !== config.value; } export function dirtyConfigKeys( configs: readonly SystemConfig[], overrides: Readonly>, ): string[] { return configs .filter((config) => { const raw = overrides[config.key]; return raw !== undefined && isConfigDirty(config, raw); }) .map((config) => config.key); } /** 行内校验:优先用 shared 定义校验,未知 key 按响应类型兜底 */ export function validateConfigRaw(config: SystemConfig, raw: string): string | null { const parsed = parseConfigRaw(config, raw); if (parsed === null) { return config.type === 'number' ? '请输入有效数字' : '请输入合法值'; } const definition = systemConfigDefinition(config.key); if (definition !== undefined) { const result = validateSystemConfigValue(definition, parsed); return result.ok ? null : result.message; } if (config.type === 'pattern') { const check = validateNamingTemplate(String(parsed), { maxLength: config.maxLength ?? 100 }); return check.valid ? null : check.errors.join(';'); } if (typeof parsed === 'string' && config.maxLength !== null && parsed.length > config.maxLength) { return `配置值不能超过 ${config.maxLength} 个字符`; } return null; } export interface ConfigChangeDraft { config: SystemConfig; value: SystemConfigValue; oldValue: SystemConfigValue; } export interface CollectConfigChangesResult { changes: ConfigChangeDraft[]; errors: Record; } /** 保存前收集脏项 + 校验;错误按 key 返回给行内提示,失败时保留草稿可重试 */ export function collectConfigChanges( configs: readonly SystemConfig[], overrides: Readonly>, ): CollectConfigChangesResult { const changes: ConfigChangeDraft[] = []; const errors: Record = {}; for (const config of configs) { const raw = overrides[config.key]; if (raw === undefined) continue; if (!isConfigDirty(config, raw)) continue; const error = validateConfigRaw(config, raw); if (error !== null) { errors[config.key] = error; continue; } const parsed = parseConfigRaw(config, raw); if (parsed === null) continue; changes.push({ config, value: parsed, oldValue: config.value }); } return { changes, errors }; } /** 阈值 / 上限类配置修改必须先二次确认(R-26) */ export function isGuardedConfig(config: SystemConfig): boolean { return config.group === 'limits' || config.group === 'threshold'; } export function hasGuardedChange(changes: readonly ConfigChangeDraft[]): boolean { return changes.some((change) => isGuardedConfig(change.config)); } export function latestUpdatedAt(items: readonly { updatedAt: string }[]): string | null { let latest: string | null = null; for (const item of items) { if (latest === null || item.updatedAt > latest) latest = item.updatedAt; } return latest; } export function formatConfigValue( value: unknown, options: { type?: SystemConfig['type']; unit?: string | null } = {}, ): string { if (value === null || value === undefined) return '未配置'; if (options.type === 'boolean') return value === true || value === 'true' ? '开启' : '关闭'; if (typeof value !== 'string' && typeof value !== 'number' && typeof value !== 'boolean') { return '—'; } if (options.unit !== null && options.unit !== undefined && options.unit !== '') { return `${String(value)} ${options.unit}`; } return String(value); } export function configValueText(config: SystemConfig, value: unknown): string { return formatConfigValue(value, { type: config.type, unit: config.unit }); } /** MM-DD HH:mm(系统配置页统一时间展示) */ export function formatConfigTime(iso: string | null): string { if (iso === null || iso === '') return '—'; const date = new Date(iso); if (Number.isNaN(date.getTime())) return '—'; const pad = (value: number): string => String(value).padStart(2, '0'); return `${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`; } /** 数值范围 / 模板长度提示 */ export function rangeTextOf(config: SystemConfig): string { if (config.type === 'number' && (config.min !== null || config.max !== null)) { const min = config.min ?? ''; const max = config.max ?? ''; return `范围 ${String(min)}–${String(max)}${config.unit === null ? '' : ` ${config.unit}`}`; } if (config.type === 'pattern') { return `≤ ${config.maxLength ?? 100} 字符`; } return ''; } // --------------------------------------------------------------------------- // 命名规则预览(与保存前校验共用 shared 函数) // --------------------------------------------------------------------------- /** 预览用示例 context:电商 / 花间集 / 口播 / 20261104 / 01 / 1080x1920 */ export const NAMING_SAMPLE_CONTEXT: NamingContext = { industry: '电商', customer: '花间集', direction: '口播', date: '20261104', sequence: '01', spec: '1080x1920', }; export interface NamingPreviewResult { check: NamingTemplateCheck; resolved: string | null; error: string | null; } export function namingPreview(pattern: string, maxLength = 100): NamingPreviewResult { const check = validateNamingTemplate(pattern, { maxLength }); if (!check.valid) { return { check, resolved: null, error: check.errors.join(';') }; } try { return { check, resolved: resolveNamingTemplate(pattern, NAMING_SAMPLE_CONTEXT, { maxLength }), error: null, }; } catch (cause) { return { check, resolved: null, error: cause instanceof Error ? cause.message : '命名预览失败', }; } }