diff --git a/client/lib/core/theme/app_tokens.g.dart b/client/lib/core/theme/app_tokens.g.dart new file mode 100644 index 0000000..eaffd7c --- /dev/null +++ b/client/lib/core/theme/app_tokens.g.dart @@ -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; + } +} diff --git a/client/lib/core/theme/token_source/tokens.css b/client/lib/core/theme/token_source/tokens.css new file mode 100644 index 0000000..8db1721 --- /dev/null +++ b/client/lib/core/theme/token_source/tokens.css @@ -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; +} diff --git a/client/test/theme/app_tokens_values_test.dart b/client/test/theme/app_tokens_values_test.dart new file mode 100644 index 0000000..126eb6d --- /dev/null +++ b/client/test/theme/app_tokens_values_test.dart @@ -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); + }); +} diff --git a/client/tool/gen_tokens.mjs b/client/tool/gen_tokens.mjs new file mode 100644 index 0000000..ea319ac --- /dev/null +++ b/client/tool/gen_tokens.mjs @@ -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');