金融设计系统的合规性设计:强制色彩、风险标识与监管要求
金融设计系统的合规性设计强制色彩、风险标识与监管要求一、引言当设计系统撞上金融监管美学的边界就是法律的边界在非金融行业做设计系统时我追求的通常是一致性、可扩展性和美学品质。但当我第一次参与一个金融科技产品的设计系统重构时一个资深风控同事递给我一份 74 页的文档——《证券期货投资者适当性管理办法》及配套指南。他说你的设计系统里每一个颜色、每一个标签、每一个提示文案都在这份文档里有对应的规定。我翻开文档第一次意识到金融设计系统不是设计规范而是法规范本。你使用的红色必须精确到 hex 值因为监管机构要截图存档高风险和潜在风险的措辞差别可能导致用户投诉收益二字前面必须紧跟着不保证预期收益率这个词根本就不能出现只能使用业绩比较基准。更复杂的是不同金融子领域有不同的监管要求。证券、基金、保险、银行——它们的色彩使用、信息披露、风险提示格式要求各不相同。一个服务于多家金融机构的 SaaS 产品它的设计系统必须同时满足多个监管体系的要求。这就是金融设计系统合规性设计的核心挑战如何在一个统一的设计系统中嵌入多层监管要求并且保证不同场景下不会出现合规冲突二、底层机制与原理深度剖析风险分级色彩体系金融产品的风险等级划分在监管文件中有明确要求设计系统必须建立严格的风险色彩体系R1低风险蓝色或绿色不能使用红色系任何颜色R2中低风险浅蓝色R3中风险黄色/橙色R4中高风险深橙色R5高风险红色强制信息展示规则监管要求的设计规则中最核心的是强制信息展示规则产品名称必须包含风险等级风险提示文案必须在首屏可见历史业绩不代表未来表现必须出现在任何展示收益率的位置附近数据更新时间必须与数据同时展示三、生产级代码实现/** * 金融设计系统合规引擎 * * 核心功能 * 1. 风险色彩严格映射不可被组件随意覆盖 * 2. 强制信息展示验证运行时检查 * 3. 合规审计日志可追溯 */ /** 金融产品风险等级 */ enum RiskLevel { R1_LOW R1, R2_MEDIUM_LOW R2, R3_MEDIUM R3, R4_MEDIUM_HIGH R4, R5_HIGH R5 } /** 合规设计 Token —— 这些值不可被覆盖 */ const ComplianceTokens { /** 风险等级色彩 —— 严格遵守监管规范 */ riskColor: { [RiskLevel.R1_LOW]: { primary: #1890FF, // 蓝色绝对不能用红色 background: #E6F7FF, text: #0050B3 }, [RiskLevel.R5_HIGH]: { primary: #D9363E, // 红色必须醒目 background: #FFF1F0, text: #A8071A } // ... 其他等级 } as const, /** 强制免责声明文案 */ disclaimers: { performance: 历史业绩不代表未来表现投资需谨慎, risk: 本产品风险等级为{RISK_LEVEL}请确认您已了解相关风险, pastReturn: 该收益率为历史业绩不构成未来收益承诺, noGuarantee: 本产品为非保本浮动收益型产品不保证本金和收益 } as const, /** 最小对比度要求WCAG AA */ minContrastRatio: 4.5 }; /** * 不合规项类型 */ interface ComplianceViolation { /** 违规代码 */ code: string; /** 违规描述 */ message: string; /** 严重程度 */ severity: error | warning; /** 违规位置信息 */ location: { component: string; prop?: string }; } /** * 运行时合规检查引擎 * * 在开发环境或 CI/CD 流程中运行 * 扫描渲染后的 DOM 树检测合规问题 */ class ComplianceRuntimeChecker { /** * 检查页面是否满足所有合规要求 */ checkPage(rootElement: HTMLElement): ComplianceViolation[] { const violations: ComplianceViolation[] []; // 检查 1: 强制风险提示是否存在 violations.push(...this.checkRiskDisclosure(rootElement)); // 检查 2: 风险等级色彩是否正确 violations.push(...this.checkRiskColors(rootElement)); // 检查 3: 文本对比度是否达标 violations.push(...this.checkContrastRatio(rootElement)); // 检查 4: 免责声明是否完整 violations.push(...this.checkDisclaimers(rootElement)); return violations; } /** * 检查 1: 风险提示必须在页面首屏可见 */ private checkRiskDisclosure(root: HTMLElement): ComplianceViolation[] { const violations: ComplianceViolation[] []; // 查找是否有 .risk-disclosure 元素 const disclosure root.querySelector([data-compliancerisk-disclosure]); if (!disclosure) { violations.push({ code: CMP-001, message: 页面缺少风险提示信息违反了金融产品信息披露规定, severity: error, location: { component: Page } }); return violations; } // 检查是否在首屏距顶部 viewport 高度 const rect disclosure.getBoundingClientRect(); const viewportHeight window.innerHeight; if (rect.top viewportHeight) { violations.push({ code: CMP-002, message: 风险提示信息不在首屏可见区域内需要上移, severity: error, location: { component: RiskDisclosure } }); } // 检查是否包含风险等级文字 const text disclosure.textContent || ; const riskLevels [RiskLevel.R1_LOW, RiskLevel.R5_HIGH]; const hasRiskLevel riskLevels.some(r text.includes(r)); if (!hasRiskLevel) { violations.push({ code: CMP-003, message: 风险提示中未包含产品风险等级信息, severity: error, location: { component: RiskDisclosure } }); } return violations; } /** * 检查 2: 风险等级与颜色的严格对应关系 */ private checkRiskColors(root: HTMLElement): ComplianceViolation[] { const violations: ComplianceViolation[] []; // 查找所有使用风险等级的组件 const riskElements root.querySelectorAll([data-risk-level]); riskElements.forEach((el) { const riskLevel el.getAttribute(data-risk-level) as RiskLevel; const computedStyle window.getComputedStyle(el); const color computedStyle.color; // 获取该风险等级规定的颜色 const expectedColor ComplianceTokens.riskColor[riskLevel]?.primary; if (!expectedColor) return; // 检查实际颜色是否与规定颜色匹配 if (!this.colorsMatch(color, expectedColor)) { violations.push({ code: CMP-004, message: 风险等级 ${riskLevel} 的元素使用了非规定颜色 ${color}应为 ${expectedColor}, severity: error, location: { component: el.tagName.toLowerCase(), prop: color } }); } }); return violations; } /** * 检查 3: 文本对比度 * * 使用 WCAG 相对亮度公式计算 */ private checkContrastRatio(root: HTMLElement): ComplianceViolation[] { const violations: ComplianceViolation[] []; // 收集所有可见文本元素 const textElements this.getVisibleTextElements(root); for (const el of textElements) { const style window.getComputedStyle(el); const bgColor this.getActualBackgroundColor(el); const ratio this.calculateContrastRatio(style.color, bgColor); if (ratio ComplianceTokens.minContrastRatio) { const text el.textContent?.substring(0, 30); violations.push({ code: CMP-005, message: 文本${text}...的对比度为 ${ratio.toFixed(2)}:1低于要求的 ${ComplianceTokens.minContrastRatio}:1, severity: error, location: { component: el.tagName.toLowerCase() } }); } } return violations; } /** * 检查 4: 免责声明完整性 */ private checkDisclaimers(root: HTMLElement): ComplianceViolation[] { const violations: ComplianceViolation[] []; const pageText root.textContent || ; // 检查是否包含历史业绩不代表未来表现 if (!pageText.includes(不代表未来表现) !pageText.includes(不构成)) { // 仅在页面有收益率展示时检查 const hasPerformance root.querySelector([data-typeperformance]); if (hasPerformance) { violations.push({ code: CMP-006, message: 展示历史收益率时缺少历史业绩不代表未来表现的免责声明, severity: error, location: { component: PerformanceDisplay } }); } } return violations; } /** * 计算两个颜色的对比度WCAG 2.1 公式 */ private calculateContrastRatio(color1: string, color2: string): number { const l1 this.getRelativeLuminance(color1); const l2 this.getRelativeLuminance(color2); const lighter Math.max(l1, l2); const darker Math.min(l1, l2); return (lighter 0.05) / (darker 0.05); } private getRelativeLuminance(color: string): number { // 解析颜色并计算相对亮度 const rgb this.parseColorToRgb(color); const [r, g, b] rgb.map(c { const s c / 255; return s 0.03928 ? s / 12.92 : Math.pow((s 0.055) / 1.055, 2.4); }); return 0.2126 * r 0.7152 * g 0.0722 * b; } // 辅助方法 private colorsMatch(a: string, b: string): boolean { return a.toLowerCase().replace(/\s/g, ) b.toLowerCase().replace(/\s/g, ); } private getVisibleTextElements(root: HTMLElement): HTMLElement[] { // 简化实现获取所有有文本内容的元素 const walker document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT); const elements: HTMLElement[] []; let node: Node | null; while ((node walker.nextNode())) { const el node as HTMLElement; if (el.textContent?.trim()) { elements.push(el); } } return elements; } private getActualBackgroundColor(el: HTMLElement): string { let current: HTMLElement | null el; while (current) { const bg window.getComputedStyle(current).backgroundColor; if (bg bg ! rgba(0, 0, 0, 0) bg ! transparent) { return bg; } current current.parentElement; } return #FFFFFF; // 默认白色背景 } private parseColorToRgb(color: string): [number, number, number] { const div document.createElement(div); div.style.color color; document.body.appendChild(div); const computed window.getComputedStyle(div).color; document.body.removeChild(div); const match computed.match(/rgb\((\d),\s*(\d),\s*(\d)\)/); return match ? [parseInt(match[1]), parseInt(match[2]), parseInt(match[3])] : [0, 0, 0]; } } /** * 设计系统 Token 守卫 * * 确保开发者不能通过 Prop 覆盖合规要求的 Token * 使用 Proxy 拦截不合规的属性赋值 */ function createComplianceGuardT extends Recordstring, unknown( tokens: T, lockedKeys: (keyof T)[] ): T { return new Proxy(tokens, { set(target, prop, value) { if (lockedKeys.includes(prop as keyof T)) { console.error( [合规警告] Token ${String(prop)} 受合规保护不允许动态修改。 当前值为 ${target[prop as keyof T]}尝试设置为 ${value} ); return false; // 拒绝修改 } (target as any)[prop] value; return true; } }); }四、边界分析与架构权衡关键缺点运行时检查的性能消耗。全页面 DOM 扫描检查对比度是一个昂贵的操作不应该在生产环境每次渲染后执行。建议仅作为 CI/CD 流程中的自动化检查。监管规则的多变性。金融监管政策会不断更新设计系统的合规 Token 必须随之更新。这意味着版本管理和变更追溯的复杂度远超普通设计系统。过度合规风险。过于积极的合规检查可能导致大量误报降低团队的采纳意愿。需要平衡必须检查和建议检查。国际化复杂性。合规要求在不同国家/地区差异巨大。一个在中国合规的设计系统在美国可能完全不适用。适用边界适用不适用面向中国大陆金融用户内部测试环境有明确监管要求的产品非金融行业需要向监管机构报备非上市公司的内部系统五、总结金融设计系统的合规性设计是一份戴着镣铐的舞蹈。你不能像在普通项目中那样自由地选择颜色、排列元素、撰写文案——每一个设计决策背后都站着一份监管文件。但这不意味着金融设计系统就只能是丑陋的合规表格。在合规的框架内仍然有巨大的创造空间如何让风险提示信息更具可读性如何让复杂的信息披露在移动端依然清晰如何在严格的色彩约束下建立情感连接我最大的感悟是金融设计系统的核心竞争力不是设计创新能力而是在约束条件下寻找最优解的能力。这种能力不是天生的而是在一次次合规审计的反饋中积累出来的。所以我的建议是找一个资深的风控合规同事做你的 Design Partner让他们参与设计系统的评审会。他们的视角虽然不总是设计友好的但至少可以避免你在犯低级合规错误上花费不必要的成本。作者李慕杰Leo / 8limujie一个学会了阅读监管文件的前端匠人