feat(client): codegen 单源生成三主题 AppTokens(禁手写 hex)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-06-24 21:36:22 +08:00
parent 5b9ac47362
commit c5818b4e51
4 changed files with 334 additions and 0 deletions
+159
View File
@@ -0,0 +1,159 @@
// GENERATED by tool/gen_tokens.mjs — DO NOT EDIT.
// 源:lib/core/theme/token_source/tokens.css(从原型同步)。改色请改源后重跑脚本。
import 'package:flutter/material.dart';
import 'app_tokens.dart';
const AppTokens kTokensA = AppTokens(
page: Color(0xFFE7EBF1),
bg: Color(0xFFF5F7FA),
surface: Color(0xFFFFFFFF),
border: Color(0xFFDCE2EB),
borderSubtle: Color(0xFFECEFF4),
text: Color(0xFF232934),
heading: Color(0xFF1B2430),
title: Color(0xFF1B2430),
muted: Color(0xFF6E7888),
faint: Color(0xFF99A3B3),
primary: Color(0xFF2563AC),
primaryDark: Color(0xFF154072),
onPrimary: Color(0xFFFFFFFF),
brand400: Color(0xFF4F86C6),
brand50: Color(0xFFEEF4FB),
accent: Color(0xFF8B2331),
success: Color(0xFF2E8B57),
danger: Color(0xFFD14343),
warn: Color(0xFFE08E00),
infoSoft: Color(0xFFEEF4FB),
okSoft: Color(0xFFE8F5EE),
accentSoft: Color(0xFFFAEEF0),
successBg: Color(0xFFE8F5EE),
dangerBg: Color(0xFFFDECEC),
warnBg: Color(0xFFFFF4DB),
thBg: Color(0xFFF7F9FC),
rowHover: Color(0xFFEEF4FB),
shadow: Color(0x33142846),
topBg: Color(0xFF2563AC),
topFg: Color(0xFFFFFFFF),
topFaint: Color(0xb8FFFFFF),
topBorder: Color(0xFF1E5594),
topControl: Color(0x29FFFFFF),
topMarkBg: Color(0x38FFFFFF),
topMarkFg: Color(0xFFFFFFFF),
sideBg: Color(0xFF163A6C),
sideFg: Color(0xFFC4D6ED),
sideMuted: Color(0xFF7C9FCB),
sideActiveBg: Color(0xFF2C6BB3),
sideActiveFg: Color(0xFFFFFFFF),
sideBorder: Color(0xFF1E4A86),
sideHover: Color(0xFF1C4680),
menuUserBg: Color(0xFF3C79C0),
menuUserFg: Color(0xFFFFFFFF),
menuUserHover: Color(0xFF4D88CC),
isDark: false,
);
const AppTokens kTokensB = AppTokens(
page: Color(0xFF0B0B10),
bg: Color(0xFF15151C),
surface: Color(0xFF1E1E27),
border: Color(0xFF2B2B37),
borderSubtle: Color(0xFF26262F),
text: Color(0xFFE8E8EE),
heading: Color(0xFFF6F6F9),
title: Color(0xFFF6F6F9),
muted: Color(0xFF9A9AA8),
faint: Color(0xFF6E6E7E),
primary: Color(0xFFF5A300),
primaryDark: Color(0xFFD98C00),
onPrimary: Color(0xFF1A1408),
brand400: Color(0xFFE0A23A),
brand50: Color(0xFF2A2212),
accent: Color(0xFFF87171),
success: Color(0xFF34D399),
danger: Color(0xFFF87171),
warn: Color(0xFFFBBF24),
infoSoft: Color(0xFF1F2530),
okSoft: Color(0xFF16312A),
accentSoft: Color(0xFF33181A),
successBg: Color(0xFF16312A),
dangerBg: Color(0xFF3A1A1C),
warnBg: Color(0xFF332811),
thBg: Color(0xFF1A1A22),
rowHover: Color(0xFF24242F),
shadow: Color(0x8c000000),
topBg: Color(0xFF1A1A22),
topFg: Color(0xFFE8E8EE),
topFaint: Color(0xFF6E6E7E),
topBorder: Color(0xFF2B2B37),
topControl: Color(0xFF15151C),
topMarkBg: Color(0xFFF5A300),
topMarkFg: Color(0xFF1A1408),
sideBg: Color(0xFF121219),
sideFg: Color(0xFF9A9AA8),
sideMuted: Color(0xFF5E5E6C),
sideActiveBg: Color(0xFF2A2212),
sideActiveFg: Color(0xFFF5A300),
sideBorder: Color(0xFF24242E),
sideHover: Color(0xFF1E1E27),
menuUserBg: Color(0xFF3A3120),
menuUserFg: Color(0xFFF0E7D6),
menuUserHover: Color(0xFF473B27),
isDark: true,
);
const AppTokens kTokensC = AppTokens(
page: Color(0xFFE9DECB),
bg: Color(0xFFFAF5EC),
surface: Color(0xFFFFFDF8),
border: Color(0xFFEADDC9),
borderSubtle: Color(0xFFF1E8D8),
text: Color(0xFF33271F),
heading: Color(0xFF241A14),
title: Color(0xFF9E3B30),
muted: Color(0xFF917A64),
faint: Color(0xFFB9A488),
primary: Color(0xFF7C1F2C),
primaryDark: Color(0xFF5F1621),
onPrimary: Color(0xFFF7E9D6),
brand400: Color(0xFFB5862F),
brand50: Color(0xFFF3E7D2),
accent: Color(0xFFA23522),
success: Color(0xFF2E6B43),
danger: Color(0xFFB23A3A),
warn: Color(0xFFB07A12),
infoSoft: Color(0xFFF1E6D2),
okSoft: Color(0xFFE7F0E6),
accentSoft: Color(0xFFF3E0D5),
successBg: Color(0xFFE7F0E6),
dangerBg: Color(0xFFF6E4DD),
warnBg: Color(0xFFFBEFCF),
thBg: Color(0xFFF6EEDF),
rowHover: Color(0xFFF5ECDB),
shadow: Color(0x384A1420),
topBg: Color(0xFF7C1F2C),
topFg: Color(0xFFF7E9D6),
topFaint: Color(0xb8F7E9D6),
topBorder: Color(0xFF671E2C),
topControl: Color(0x24FFFFFF),
topMarkBg: Color(0x33FFFFFF),
topMarkFg: Color(0xFFF7E9D6),
sideBg: Color(0xFF4A1420),
sideFg: Color(0xFFE0C9B2),
sideMuted: Color(0xFFB58E76),
sideActiveBg: Color(0xFF671E2C),
sideActiveFg: Color(0xFFF7E9D6),
sideBorder: Color(0xFF5A2330),
sideHover: Color(0xFF5A1B28),
menuUserBg: Color(0xFF7E2B3B),
menuUserFg: Color(0xFFF7E9D6),
menuUserHover: Color(0xFF8E3344),
isDark: false,
);
AppTokens appTokensOf(String key) {
switch (key) {
case 'b': return kTokensB;
case 'c': return kTokensC;
default: return kTokensA;
}
}
@@ -0,0 +1,70 @@
/* ============================================================
岩美酒库 · 令牌层(单一真源)
颜色按主题切换;间距/圆角/阴影/字号 全主题统一。
规则:屏幕与组件只引用 var(--token),禁硬编码 hex。
============================================================ */
:root{
/* 间距(4px 基) */
--sp-1:4px; --sp-2:8px; --sp-3:12px; --sp-4:16px; --sp-5:20px; --sp-6:24px; --sp-8:32px; --sp-10:40px;
/* 圆角 */
--r-sm:4px; --r-md:6px; --r-lg:10px; --r-xl:14px; --r-pill:999px;
/* 字号梯度 */
--fs-display:28px; --fs-h1:21px; --fs-h2:17px; --fs-title:15px; --fs-body:13px; --fs-sm:12px; --fs-xs:11px;
/* 字体族 */
--font:-apple-system,BlinkMacSystemFont,"PingFang SC","Microsoft YaHei","Inter",sans-serif;
--font-mono:"JetBrains Mono","SF Mono",ui-monospace,monospace;
/* 阴影系统(用各主题 --shadow 染色) */
--sh-1:0 1px 3px var(--shadow); --sh-2:0 4px 14px var(--shadow); --sh-3:0 18px 50px var(--shadow);
/* 遮罩 / 玻璃叠加(主题无关:弹窗·抽屉背板、hero 上的半透明白) */
--scrim:rgba(15,20,30,.42); --hero-glass:rgba(255,255,255,.16); --hero-glass-strong:rgba(255,255,255,.24);
/* 品牌标识色(logo / 门店字标,固定不随主题) */
--brand-deep:#0F3057; --brand-mid:#1B4F8E; --brand-wine:#8B2331;
/* 底部状态栏(深石板灰绿,主题无关的 chrome 条) */
--statusbar-bg:#37423D; --statusbar-fg:rgba(255,255,255,.82); --statusbar-muted:rgba(255,255,255,.46); --statusbar-strong:#FFFFFF; --statusbar-div:rgba(255,255,255,.13); --statusbar-ok:#7BC49A;
/* Toast 深底(主题无关) */
--toast-bg:#1F2330;
/* z-index 层级阶梯(全局唯一来源,禁止裸写 z-index≥10) */
--z-sticky:20; --z-shell-scrim:30; --z-shell-panel:35; --z-header:40; --z-fab:45;
--z-drawer-scrim:65; --z-drawer:70; --z-overlay:80; --z-pop:85; --z-menu:88; --z-toast:90;
}
/* ---------- A 经典蓝(浅 · 基准/默认) ---------- */
:root[data-theme="a"]{
--page:#E7EBF1; --bg:#F5F7FA; --surface:#FFFFFF; --border:#DCE2EB; --border-subtle:#ECEFF4;
--text:#232934; --heading:#1B2430; --title:#1B2430; --muted:#6E7888; --faint:#99A3B3;
--primary:#2563AC; --primary-dark:#154072; --on-primary:#FFFFFF; --brand400:#4F86C6; --brand50:#EEF4FB; --accent:#8B2331;
--success:#2E8B57; --danger:#D14343; --warn:#E08E00;
--info-soft:#EEF4FB; --ok-soft:#E8F5EE; --accent-soft:#FAEEF0;
--success-bg:#E8F5EE; --danger-bg:#FDECEC; --warn-bg:#FFF4DB;
--th-bg:#F7F9FC; --row-hover:#EEF4FB; --shadow:rgba(20,40,70,.20);
--top-bg:#2563AC; --top-fg:#FFFFFF; --top-faint:rgba(255,255,255,.72); --top-border:#1E5594; --top-control:rgba(255,255,255,.16); --top-mark-bg:rgba(255,255,255,.22); --top-mark-fg:#FFFFFF;
--side-bg:#163A6C; --side-fg:#C4D6ED; --side-muted:#7C9FCB; --side-active-bg:#2C6BB3; --side-active-fg:#FFFFFF; --side-border:#1E4A86; --side-hover:#1C4680;
--menu-user-bg:#3C79C0; --menu-user-fg:#FFFFFF; --menu-user-hover:#4D88CC;
}
/* ---------- B 琥珀(深) ---------- */
:root[data-theme="b"]{
--page:#0B0B10; --bg:#15151C; --surface:#1E1E27; --border:#2B2B37; --border-subtle:#26262F;
--text:#E8E8EE; --heading:#F6F6F9; --title:#F6F6F9; --muted:#9A9AA8; --faint:#6E6E7E;
--primary:#F5A300; --primary-dark:#D98C00; --on-primary:#1A1408; --brand400:#E0A23A; --brand50:#2A2212; --accent:#F87171;
--success:#34D399; --danger:#F87171; --warn:#FBBF24;
--info-soft:#1F2530; --ok-soft:#16312A; --accent-soft:#33181A;
--success-bg:#16312A; --danger-bg:#3A1A1C; --warn-bg:#332811;
--th-bg:#1A1A22; --row-hover:#24242F; --shadow:rgba(0,0,0,.55);
--top-bg:#1A1A22; --top-fg:#E8E8EE; --top-faint:#6E6E7E; --top-border:#2B2B37; --top-control:#15151C; --top-mark-bg:linear-gradient(135deg,#F5A300,#D98C00); --top-mark-fg:#1A1408;
--side-bg:#121219; --side-fg:#9A9AA8; --side-muted:#5E5E6C; --side-active-bg:#2A2212; --side-active-fg:#F5A300; --side-border:#24242E; --side-hover:#1E1E27;
--menu-user-bg:#3A3120; --menu-user-fg:#F0E7D6; --menu-user-hover:#473B27;
}
/* ---------- C 酒窖(暖浅) ---------- */
:root[data-theme="c"]{
--page:#E9DECB; --bg:#FAF5EC; --surface:#FFFDF8; --border:#EADDC9; --border-subtle:#F1E8D8;
--text:#33271F; --heading:#241A14; --title:#9E3B30; --muted:#917A64; --faint:#B9A488;
--primary:#7C1F2C; --primary-dark:#5F1621; --on-primary:#F7E9D6; --brand400:#B5862F; --brand50:#F3E7D2; --accent:#A23522;
--success:#2E6B43; --danger:#B23A3A; --warn:#B07A12;
--info-soft:#F1E6D2; --ok-soft:#E7F0E6; --accent-soft:#F3E0D5;
--success-bg:#E7F0E6; --danger-bg:#F6E4DD; --warn-bg:#FBEFCF;
--th-bg:#F6EEDF; --row-hover:#F5ECDB; --shadow:rgba(74,20,32,.22);
--top-bg:#7C1F2C; --top-fg:#F7E9D6; --top-faint:rgba(247,233,214,.72); --top-border:#671E2C; --top-control:rgba(255,255,255,.14); --top-mark-bg:rgba(255,255,255,.20); --top-mark-fg:#F7E9D6;
--side-bg:#4A1420; --side-fg:#E0C9B2; --side-muted:#B58E76; --side-active-bg:#671E2C; --side-active-fg:#F7E9D6; --side-border:#5A2330; --side-hover:#5A1B28;
--menu-user-bg:#7E2B3B; --menu-user-fg:#F7E9D6; --menu-user-hover:#8E3344;
}
@@ -0,0 +1,31 @@
// client/test/theme/app_tokens_values_test.dart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:jiu_client/core/theme/app_tokens.g.dart';
void main() {
test('A 经典蓝基准色正确', () {
expect(kTokensA.primary, const Color(0xFF2563AC));
expect(kTokensA.surface, const Color(0xFFFFFFFF));
expect(kTokensA.warn, const Color(0xFFE08E00));
expect(kTokensA.isDark, false);
});
test('B 琥珀为深色且主色为琥珀金', () {
expect(kTokensB.primary, const Color(0xFFF5A300));
expect(kTokensB.isDark, true);
// top-mark-bg 是 linear-gradient → 取首个 hex
expect(kTokensB.topMarkBg, const Color(0xFFF5A300));
});
test('C 酒窖暖浅主色为酒红', () {
expect(kTokensC.primary, const Color(0xFF7C1F2C));
expect(kTokensC.isDark, false);
});
test('shadow rgba 带 alpha 正确解析', () {
// a: rgba(20,40,70,.20) → 0x33142846
expect(kTokensA.shadow, const Color(0x33142846));
});
test('appTokensOf 回退到 A', () {
expect(appTokensOf('x'), kTokensA);
expect(appTokensOf('b'), kTokensB);
});
}
+74
View File
@@ -0,0 +1,74 @@
// client/tool/gen_tokens.mjs
// 解析 token_source/tokens.css 的三套主题块 → 生成 app_tokens.g.dart。
// 用法:node tool/gen_tokens.mjs (在 client/ 下运行)
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const css = fs.readFileSync(path.join(ROOT, 'lib/core/theme/token_source/tokens.css'), 'utf8');
// CSS 变量名(kebab) → dart 字段(camel)。仅这些进 AppTokens。
const FIELDS = {
'page':'page','bg':'bg','surface':'surface','border':'border','border-subtle':'borderSubtle',
'text':'text','heading':'heading','title':'title','muted':'muted','faint':'faint',
'primary':'primary','primary-dark':'primaryDark','on-primary':'onPrimary','brand400':'brand400',
'brand50':'brand50','accent':'accent','success':'success','danger':'danger','warn':'warn',
'info-soft':'infoSoft','ok-soft':'okSoft','accent-soft':'accentSoft','success-bg':'successBg',
'danger-bg':'dangerBg','warn-bg':'warnBg','th-bg':'thBg','row-hover':'rowHover','shadow':'shadow',
'top-bg':'topBg','top-fg':'topFg','top-faint':'topFaint','top-border':'topBorder','top-control':'topControl',
'top-mark-bg':'topMarkBg','top-mark-fg':'topMarkFg','side-bg':'sideBg','side-fg':'sideFg',
'side-muted':'sideMuted','side-active-bg':'sideActiveBg','side-active-fg':'sideActiveFg',
'side-border':'sideBorder','side-hover':'sideHover','menu-user-bg':'menuUserBg',
'menu-user-fg':'menuUserFg','menu-user-hover':'menuUserHover',
};
const DARK = { a:false, b:true, c:false };
function block(key){
const m = css.match(new RegExp(`\\[data-theme="${key}"\\]\\s*\\{([^}]*)\\}`));
if(!m) throw new Error('missing theme block: '+key);
const out = {};
for(const mm of m[1].matchAll(/--([a-z0-9-]+)\s*:\s*([^;]+);/g)) out[mm[1]] = mm[2].trim();
return out;
}
function toColor(raw){
let v = raw.trim();
if(v.startsWith('linear-gradient')){ const h = v.match(/#([0-9a-fA-F]{6})/); v = '#'+h[1]; } // 取首个 hex
let m = v.match(/^#([0-9a-fA-F]{6})$/);
if(m) return `Color(0xFF${m[1].toUpperCase()})`;
m = v.match(/^rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*(?:,\s*([\d.]+))?\s*\)$/);
if(m){ const a = Math.round((m[4]===undefined?1:parseFloat(m[4]))*255).toString(16).padStart(2,'0');
const hex = [m[1],m[2],m[3]].map(n=>(+n).toString(16).padStart(2,'0')).join(''); return `Color(0x${a}${hex.toUpperCase()})`; }
throw new Error('unsupported color value: '+raw);
}
function emit(key){
const b = block(key);
const lines = Object.entries(FIELDS).map(([css,dart])=>{
if(b[css]===undefined) throw new Error(`theme ${key} missing --${css}`);
return ` ${dart}: ${toColor(b[css])},`;
});
lines.push(` isDark: ${DARK[key]},`);
return `const AppTokens kTokens${key.toUpperCase()} = AppTokens(\n${lines.join('\n')}\n);`;
}
const body = `// GENERATED by tool/gen_tokens.mjs — DO NOT EDIT.
// 源:lib/core/theme/token_source/tokens.css(从原型同步)。改色请改源后重跑脚本。
import 'package:flutter/material.dart';
import 'app_tokens.dart';
${emit('a')}
${emit('b')}
${emit('c')}
AppTokens appTokensOf(String key) {
switch (key) {
case 'b': return kTokensB;
case 'c': return kTokensC;
default: return kTokensA;
}
}
`;
fs.writeFileSync(path.join(ROOT, 'lib/core/theme/app_tokens.g.dart'), body);
console.log('generated app_tokens.g.dart');