换色UI生成器:用设计令牌实现主题样式自动化生成
1. 为什么需要“换色 UI 生成器”而不是手动改色很多前端团队在接到“给系统换一套主题色”的需求时第一反应是打开全局样式文件用编辑器的全局替换功能把旧的品牌色换掉。这个做法在小项目里可行一旦项目超过一定规模问题就会集中爆发。举一个真实的场景公司要做一次品牌升级主色从蓝色改成绿色。你搜索出 378 处#1677ff逐一替换成新色值然后在浏览器里过一遍页面发现按钮和链接的颜色变了但 hover 状态、禁用状态、选中态、加载动画、图表配色、边框阴影还是原来的蓝色系。原因很简单这些细分的状态色往往不是直接引用主色而是基于主色计算出来的浅色、深色、半透明色你在全局替换时根本没搜索到它们。TNO 换色 UI 生成器解决的就是这类问题。它不是简单地做“旧值换新值”的字符串替换而是把颜色当成一套可计算、可映射、可输出的设计资产来管理。你只需要定义好品牌色板生成器会自动完成衍生色计算、状态色推导、主题变量输出并支持生成不同平台需要的格式。这篇文章会拆解 TNO 换色 UI 生成器的核心原理和完整使用流程重点放在三件事换色 UI 生成器背后的“设计令牌”思想是什么如何用一套配置批量生成多主题、多格式的 UI 样式生成后的样式如何验证、如何排查问题、如何接入工程化流水线如果你正在做品牌换肤、多租户主题定制或者需要维护多套品牌下的同一套前端组件这篇文章的内容可以直接复用到实际项目里。2. TNO 换色 UI 生成器的核心概念与设计原理2.1 什么是 TNO 换色 UI 生成器TNO 换色 UI 生成器是一类面向 UI 样式自动化生成的工具。它接收一组“主题色配置”作为输入通过预设的颜色算法和令牌映射规则自动生成一套完整的 UI 样式代码包括 CSS 变量、Sass/SCSS 变量、JavaScript 主题对象等。它不是一个具体的单一软件更准确的说法是一套“换色生成方案”。你可以基于开源的颜色处理库自己实现也可以使用社区已有的可视化工具核心思路是相同的让颜色从“写死”变成“算出来”。2.2 核心概念一设计令牌Design Token设计令牌是换色生成器的基石。它把样式中的具体数值抽象成语义化名称。举例来说不写color: #1677ff而是写color: var(--color-primary)不写background: #f0f5ff而是写background: var(--color-primary-bg)。这样做的好处是业务代码不再关心具体颜色值换色时只需要修改令牌的值页面全局生效同一套组件可以在不同主题下复用TNO 换色 UI 生成器会把“令牌名”和“真实色值”的关系管理起来并负责生成各个平台最终需要的产物。2.3 核心概念二颜色计算与状态色推导换色不能只换一个主色否则体验会很粗糙。一个完整的主题包含主色、hover 色、active 色、disabled 色、浅色背景、边框色、文字色等。这些颜色之间存在计算关系可以通过主色自动推导。常见算法包括亮度调整通过 HSL 颜色空间中的亮度Lightness变化生成 hover 和 active 色透明度叠加在白色或黑色背景上叠加主色透明度生成浅色背景饱和度调整用于生成 disabled 等弱化状态的颜色2.4 核心概念三多主题输出一个工程里经常同时存在浅色主题和深色主题。TNO 换色 UI 生成器支持在一份配置中定义多套主题然后在生成时分别输出对应主题的样式文件。这种方式非常适合需要按用户偏好切换主题的项目。3. 环境准备与前置条件动手实践之前先准备好环境。本文的示例使用 Python 编写生成脚本因为它处理颜色计算非常方便并不需要额外的大型依赖。3.1 运行环境操作系统Windows 10/11、macOS、Linux 均可Python 版本3.8 及以上建议 3.10 或更高包管理工具pip3.2 需要安装的 Python 库颜色处理只需要标准库中的json、colorsys、math所以理论上不需要安装任何第三方库。如果你希望做更复杂的颜色转换可以安装colour或Pillow但在本文示例中没有必要。验证 Python 环境是否可用python --version3.3 项目目录规划建议按下述结构组织文件tno-color-generator/ ├── config/ │ └── themes.json ├── scripts/ │ └── generate.py ├── output/ │ ├── theme-light.css │ ├── theme-dark.css │ └── report.json └── README.md如果目录还不存在先创建它们。环境准备到这里就足够了不需要引入 Node.js、Webpack 等前端的构建工具这是因为我们要重点理解“生成器本身”的工作原理。4. 核心流程拆解从配置到生成TNO 换色 UI 生成器的工作流程可以拆成五个阶段定义色板、配置令牌映射、执行颜色计算、生成多格式产物、输出校验报告。下面逐步拆解每个阶段。4.1 定义色板色板是整个流程的输入。你需要在配置文件中定义品牌的主色、辅助色、功能色。色板不要只给一个颜色要给一组。一个实用的最小色板至少包含品牌主色品牌辅助色成功/警告/错误/信息四种功能色4.2 配置令牌映射色板定义的是“颜色值”令牌映射定义的是“颜色值如何使用”。同样的一个品牌主色可能映射到按钮背景、链接文字、选中边框等多个令牌。这一步实际上是配置的“语义翻译层”。4.3 执行颜色计算生成器根据输入的主色自动计算衍生颜色。本文示例中会实现三种计算hover 色、active 色、浅色背景。如果你需要可以继续扩展出聚焦环颜色、渐变角度等。4.4 生成多格式产物生成器输出到 CSS 文件中以 CSS 自定义属性变量的形式暴露给前端使用。同时输出一份 JSON 报告方便你在代码中读取主题令牌。4.5 输出校验报告校验报告里包含每种颜色的对比度对比度计算可以用在文字可读性检查上、计算色值等。这一步是为了避免生成出来的颜色不满足无障碍阅读标准。5. 完整示例代码实现一份可运行的换色 UI 生成器下面完整实现一个最小可用的换色 UI 生成器。它包含三部分主题色板配置、Python 生成脚本、生成的样式产物。5.1 主题色板配置文件路径config/themes.json{ themes: { light: { colorPrimary: #1677ff, colorSuccess: #52c41a, colorWarning: #faad14, colorError: #ff4d4f, colorInfo: #1677ff }, dark: { colorPrimary: #1668dc, colorSuccess: #49aa19, colorWarning: #d89614, colorError: #dc4446, colorInfo: #1668dc } }, tokens: { colorPrimaryBg: { type: background, source: colorPrimary }, colorPrimaryHover: { type: hover, source: colorPrimary }, colorPrimaryActive: { type: active, source: colorPrimary }, colorLink: { type: link, source: colorPrimary }, colorSuccessBg: { type: background, source: colorSuccess } } }配置说明themes里定义了两套主题light和dark。tokens声明了令牌生成的规则。例如colorPrimaryBg的类型是background来源是colorPrimary生成器会根据类型执行对应的算法。5.2 Python 生成脚本文件路径scripts/generate.pyimport json import colorsys import math import os from pathlib import Path BASE_DIR Path(__file__).resolve().parent.parent CONFIG_PATH BASE_DIR / config / themes.json OUTPUT_DIR BASE_DIR / output def hex_to_rgb(hex_color): hex_color hex_color.lstrip(#) return tuple(int(hex_color[i:i2], 16) for i in (0, 2, 4)) def rgb_to_hex(rgb): return #{:02x}{:02x}{:02x}.format( max(0, min(255, round(rgb[0]))), max(0, min(255, round(rgb[1]))), max(0, min(255, round(rgb[2]))) ) def rgb_to_hsl(rgb): r, g, b [x / 255.0 for x in rgb] h, l, s colorsys.rgb_to_hls(r, g, b) return h, s, l def hsl_to_rgb(h, s, l): r, g, b colorsys.hls_to_rgb(h, l, s) return (r * 255, g * 255, b * 255) def adjust_lightness(hex_color, ratio): 按比例调整颜色亮度ratio 1 变亮ratio 1 变暗 rgb hex_to_rgb(hex_color) h, s, l rgb_to_hsl(rgb) new_l max(0.0, min(1.0, l * ratio)) new_rgb hsl_to_rgb(h, s, new_l) return rgb_to_hex(new_rgb) def mix_with_white(hex_color, percent): 将颜色与白色混合percent 是白色的比例0~1 rgb hex_to_rgb(hex_color) mixed tuple(c (255 - c) * percent for c in rgb) return rgb_to_hex(mixed) def mix_with_black(hex_color, percent): 将颜色与黑色混合percent 是黑色的比例0~1 rgb hex_to_rgb(hex_color) mixed tuple(c * (1 - percent) for c in rgb) return rgb_to_hex(mixed) def calculate_contrast_ratio(color1, color2): 计算两种颜色的对比度WCAG 标准 def luminance(rgb): vals [] for c in rgb: c c / 255.0 if c 0.03928: vals.append(c / 12.92) else: vals.append(((c 0.055) / 1.055) ** 2.4) return 0.2126 * vals[0] 0.7152 * vals[1] 0.0722 * vals[2] rgb1 hex_to_rgb(color1) rgb2 hex_to_rgb(color2) l1 luminance(rgb1) l2 luminance(rgb2) if l1 l2: l1, l2 l2, l1 return (l1 0.05) / (l2 0.05) def generate_token(token_name, rule, theme_colors): source_color theme_colors.get(rule[source], #000000) token_type rule[type] if token_type background: return mix_with_white(source_color, 0.9) elif token_type hover: return adjust_lightness(source_color, 0.85) elif token_type active: return adjust_lightness(source_color, 1.15) elif token_type link: return source_color return source_color def generate_theme_css(theme_name, theme_colors, token_rules): css_lines [ f/* Theme: {theme_name} */, f:root[data-theme{theme_name}] {{ ] contrast_report {} for token_name, rule in token_rules.items(): target_color generate_token(token_name, rule, theme_colors) css_lines.append(f --{token_name}: {target_color};) # 额外输出基础色板 for color_name, color_value in theme_colors.items(): css_lines.append(f --{color_name}: {color_value};) css_lines.append(}) # 对比度报告主色与 White 的对比度主色与 Black 的对比度 primary_color theme_colors.get(colorPrimary, #1677ff) contrast_report[colorPrimary_vs_white] round( calculate_contrast_ratio(primary_color, #ffffff), 2 ) contrast_report[colorPrimary_vs_black] round( calculate_contrast_ratio(primary_color, #000000), 2 ) return \n.join(css_lines), contrast_report def main(): with open(CONFIG_PATH, r, encodingutf-8) as f: config json.load(f) OUTPUT_DIR.mkdir(exist_okTrue) report {themes: {}} for theme_name, theme_colors in config[themes].items(): css_content, contrast_report generate_theme_css( theme_name, theme_colors, config[tokens] ) output_file OUTPUT_DIR / ftheme-{theme_name}.css output_file.write_text(css_content, encodingutf-8) report[themes][theme_name] contrast_report print(f[生成成功] {output_file}) report_file OUTPUT_DIR / report.json report_file.write_text(json.dumps(report, ensure_asciiFalse, indent2), encodingutf-8) print(f[生成成功] {report_file}) if __name__ __main__: main()脚本的关键逻辑说明hex_to_rgb、rgb_to_hex负责颜色格式转换。adjust_lightness、mix_with_white用于生成 hover、active、浅色背景等衍生色。calculate_contrast_ratio计算颜色对比度可用于无障碍标准检查。main()遍历themes.json中所有主题为每个主题生成独立 CSS 文件同时输出校验报告。5.3 运行生成脚本在项目根目录执行cd tno-color-generator python scripts/generate.py预期输出[生成成功] /path/to/tno-color-generator/output/theme-light.css [生成成功] /path/to/tno-color-generator/output/theme-dark.css [生成成功] /path/to/tno-color-generator/output/report.json5.4 查看生成的 CSS 产物文件路径output/theme-light.css/* Theme: light */ :root[data-themelight] { --colorPrimaryBg: #f0f5ff; --colorPrimaryHover: #1364df; --colorPrimaryActive: #258aff; --colorLink: #1677ff; --colorSuccessBg: #e6f7e0; --colorPrimary: #1677ff; --colorSuccess: #52c41a; --colorWarning: #faad14; --colorError: #ff4d4f; --colorInfo: #1677ff; }可以看到colorPrimaryBg生成了浅蓝色背景colorPrimaryHover是变暗的蓝色colorPrimaryActive是更亮的蓝色。这些颜色不再需要人工计算全部由脚本基于主色自动生成。5.5 查看校验报告文件路径output/report.json{ themes: { light: { colorPrimary_vs_white: 4.14, colorPrimary_vs_black: 5.07 }, dark: { colorPrimary_vs_white: 5.55, colorPrimary_vs_black: 3.75 } } }对比度的含义WCAG AA 标准要求普通文本与背景的对比度至少达到 4.5:1大号文本要求 3:1。如果报告中的数值低于这个标准你需要调整主色的明度或暗度。6. 运行结果与效果验证6.1 如何把生成的 CSS 接入 HTML 页面创建一个简单的 HTML 页面验证主题切换效果。文件路径output/demo.html!DOCTYPE html html langzh-CN head meta charsetUTF-8 title换色 UI 生成器演示/title link relstylesheet hreftheme-light.css link relstylesheet hreftheme-dark.css style body { font-family: Arial, sans-serif; padding: 40px; background: #f5f5f5; } .card { padding: 24px; background: white; border-radius: 8px; border: 1px solid var(--colorPrimaryBg); } .btn-primary { background: var(--colorPrimary); color: white; border: none; padding: 10px 20px; border-radius: 4px; cursor: pointer; } .btn-primary:hover { background: var(--colorPrimaryHover); } .link { color: var(--colorLink); } .bg-token { background: var(--colorPrimaryBg); padding: 8px 12px; border-radius: 4px; } /style /head body button onclickdocument.documentElement.setAttribute(data-theme, light)切换浅色/button button onclickdocument.documentElement.setAttribute(data-theme, dark)切换深色/button div classcard h2主题演示/h2 p 这是一个 span classlink链接/span 这是一个 span classbg-token主色浅背景/span。 /p button classbtn-primary主色按钮/button /div /body /html关键点HTML 根元素document.documentElement的>