diff --git a/.superpowers/prototype/tokens.css b/.superpowers/prototype/tokens.css index 8db1721..d762f02 100644 --- a/.superpowers/prototype/tokens.css +++ b/.superpowers/prototype/tokens.css @@ -19,7 +19,9 @@ /* 遮罩 / 玻璃叠加(主题无关:弹窗·抽屉背板、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; + --brand-deep:#0F3057; --brand-mid:#1B4F8E; --brand-wine:#8B2331; --brand-dot:#C97B86; + /* 彩色底(横幅/徽章/品牌弹窗)上的固定前景白(主题无关 chrome 前景) */ + --on-accent:#FFFFFF; --on-accent-faint:rgba(255,255,255,.72); /* 底部状态栏(深石板灰绿,主题无关的 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 深底(主题无关) */ diff --git a/client/lib/core/theme/app_chrome.g.dart b/client/lib/core/theme/app_chrome.g.dart index 12badb9..61d766d 100644 --- a/client/lib/core/theme/app_chrome.g.dart +++ b/client/lib/core/theme/app_chrome.g.dart @@ -14,6 +14,9 @@ class AppChrome { static const Color brandDeep = Color(0xFF0F3057); static const Color brandMid = Color(0xFF1B4F8E); static const Color brandWine = Color(0xFF8B2331); + static const Color brandDot = Color(0xFFC97B86); + static const Color onAccent = Color(0xFFFFFFFF); + static const Color onAccentFaint = Color(0xb8FFFFFF); static const Color scrim = Color(0x6b0F141E); static const Color heroGlass = Color(0x29FFFFFF); static const Color heroGlassStrong = Color(0x3dFFFFFF); diff --git a/client/lib/core/theme/token_source/tokens.css b/client/lib/core/theme/token_source/tokens.css index 8db1721..d762f02 100644 --- a/client/lib/core/theme/token_source/tokens.css +++ b/client/lib/core/theme/token_source/tokens.css @@ -19,7 +19,9 @@ /* 遮罩 / 玻璃叠加(主题无关:弹窗·抽屉背板、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; + --brand-deep:#0F3057; --brand-mid:#1B4F8E; --brand-wine:#8B2331; --brand-dot:#C97B86; + /* 彩色底(横幅/徽章/品牌弹窗)上的固定前景白(主题无关 chrome 前景) */ + --on-accent:#FFFFFF; --on-accent-faint:rgba(255,255,255,.72); /* 底部状态栏(深石板灰绿,主题无关的 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 深底(主题无关) */ diff --git a/client/lib/screens/shell/app_shell.dart b/client/lib/screens/shell/app_shell.dart index 69c4b30..c27d486 100644 --- a/client/lib/screens/shell/app_shell.dart +++ b/client/lib/screens/shell/app_shell.dart @@ -10,6 +10,7 @@ import '../../core/config/app_config.dart'; import '../../core/config/license_copy.dart'; import '../../core/responsive/responsive.dart'; import '../../core/theme/context_tokens.dart'; +import '../../core/theme/app_chrome.g.dart'; import '../../providers/connectivity_provider.dart'; import '../../providers/session_heartbeat.dart'; import '../../providers/shop_provider.dart'; @@ -249,11 +250,11 @@ class _AppShellState extends ConsumerState { child: Row( mainAxisSize: MainAxisSize.min, children: const [ - Icon(Icons.visibility_outlined, size: 13, color: Colors.white), + Icon(Icons.visibility_outlined, size: 13, color: AppChrome.onAccent), SizedBox(width: 4), Text('只读', style: TextStyle( - color: Colors.white, + color: AppChrome.onAccent, fontSize: 12, fontWeight: FontWeight.w600)), ], @@ -366,7 +367,7 @@ class _AppShellState extends ConsumerState { const SizedBox(width: 8), const Text('岩美酒库', style: TextStyle( - color: Colors.white, + color: AppChrome.onAccent, fontSize: 17, fontWeight: FontWeight.w700)), ], @@ -486,13 +487,13 @@ class _AppShellState extends ConsumerState { padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6), child: const Row( children: [ - Icon(Icons.wifi_off, size: 16, color: Colors.white), + Icon(Icons.wifi_off, size: 16, color: AppChrome.onAccent), SizedBox(width: 8), Expanded( child: Text( '网络连接已断开 · 当前显示离线缓存数据,恢复后将自动刷新', style: TextStyle( - color: Colors.white, + color: AppChrome.onAccent, fontSize: 13, fontWeight: FontWeight.w500), ), @@ -542,16 +543,16 @@ class _AppShellState extends ConsumerState { padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6), child: Row( children: [ - const Icon(Icons.warning_amber_rounded, size: 16, color: Colors.white), + const Icon(Icons.warning_amber_rounded, size: 16, color: AppChrome.onAccent), const SizedBox(width: 8), Expanded( child: Text(msg, - style: const TextStyle(color: Colors.white, fontSize: 13), + style: const TextStyle(color: AppChrome.onAccent, fontSize: 13), overflow: TextOverflow.ellipsis), ), TextButton( onPressed: () => context.go('/settings?tab=license'), - style: TextButton.styleFrom(foregroundColor: Colors.white70), + style: TextButton.styleFrom(foregroundColor: AppChrome.onAccentFaint), child: const Text('去激活'), ), ], @@ -562,7 +563,7 @@ class _AppShellState extends ConsumerState { void _showLicenseExpiryDialog(BuildContext ctx, LicenseInfo lic) { final (title, body) = LicenseCopy.dialog(lic); final Color titleColor = - lic.phase == 'grace' ? Colors.orange[800]! : ctx.tokens.danger; + lic.phase == 'grace' ? ctx.tokens.warn : ctx.tokens.danger; showDialog( context: ctx, barrierDismissible: true, @@ -584,7 +585,7 @@ class _AppShellState extends ConsumerState { Navigator.pop(_); ctx.go('/settings?tab=license'); }, - child: const Text('立即前往', style: TextStyle(color: Colors.white)), + child: const Text('立即前往', style: TextStyle(color: AppChrome.onAccent)), ), ], ), @@ -649,7 +650,7 @@ class _BrandMarkPainter extends CustomPainter { canvas.drawPath(peak, p); canvas.drawLine(const Offset(15, 41), const Offset(49, 41), p); canvas.drawCircle(const Offset(32, 48), 3.4, - Paint()..color = const Color(0xFFC97B86)); + Paint()..color = AppChrome.brandDot); } @override @@ -911,18 +912,18 @@ void _showShopPanel(BuildContext context, AuthUser u, ), child: Row( children: [ - const Icon(Icons.store, color: Colors.white, size: 20), + const Icon(Icons.store, color: AppChrome.onAccent, size: 20), const SizedBox(width: 10), const Text('门店信息', style: TextStyle( - color: Colors.white, + color: AppChrome.onAccent, fontSize: 16, fontWeight: FontWeight.w600)), const Spacer(), IconButton( onPressed: () => Navigator.pop(ctx), icon: const Icon(Icons.close, - color: Colors.white70, size: 18), + color: AppChrome.onAccentFaint, size: 18), padding: EdgeInsets.zero, constraints: const BoxConstraints(), ), diff --git a/client/tool/check_ds_code.mjs b/client/tool/check_ds_code.mjs new file mode 100644 index 0000000..ef0ba73 --- /dev/null +++ b/client/tool/check_ds_code.mjs @@ -0,0 +1,79 @@ +// client/tool/check_ds_code.mjs +// 代码端「设计真相源」闸 —— 与原型 check-ds.mjs 对应,把单一真源纪律延伸到 Flutter 代码。 +// 红线:颜色只走 context.tokens / AppChrome / AppDims(从 tokens.css codegen 单源), +// 禁硬编码 hex(Color(0x..))与具名色(Colors.red 等)。合理特例须显式 `// ds-ignore: 理由` 豁免。 +// +// 用法(在 client/ 下): +// node tool/check_ds_code.mjs # 全量扫描,报存量(不 fail,便于逐步清零) +// node tool/check_ds_code.mjs --strict # 全量,有违规即 exit 1 +// node tool/check_ds_code.mjs --changed # 仅扫 git 改动的 dart 文件,有违规即 exit 1(pre-commit 用) +// +// 豁免:在该行加 `// ds-ignore: <理由>`(理由必填,便于审计)。 +import fs from 'node:fs'; +import path from 'node:path'; +import { execFileSync } from 'node:child_process'; +import { fileURLToPath } from 'node:url'; + +const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); // client/ +const LIB = path.join(ROOT, 'lib'); +const args = new Set(process.argv.slice(2)); +const CHANGED = args.has('--changed'); +const STRICT = args.has('--strict') || CHANGED; + +// 违规模式:硬编码 hex 颜色;具名色(Colors.x,除 transparent=无色合理)。 +const HEX = /Color\(0x[0-9A-Fa-f]{6,8}\)/; +const NAMED = /\bColors\.(?!transparent\b)[a-zA-Z]\w*/; +const IGNORE = /\/\/\s*ds-ignore/; + +function listDartFiles() { + if (CHANGED) { + let out = ''; + try { + out = execFileSync( + 'git', ['diff', '--name-only', '--diff-filter=ACM', 'HEAD'], + { cwd: ROOT, encoding: 'utf8' }); + } catch { out = ''; } + return out.split('\n') + .map((s) => s.trim()) + .filter((f) => f.endsWith('.dart') && !f.endsWith('.g.dart')) + .map((f) => path.resolve(ROOT, '..', f)) // git 路径相对仓库根 + .filter((f) => f.startsWith(LIB) && fs.existsSync(f)); + } + const out = []; + (function walk(d) { + for (const e of fs.readdirSync(d, { withFileTypes: true })) { + const p = path.join(d, e.name); + if (e.isDirectory()) walk(p); + else if (e.name.endsWith('.dart') && !e.name.endsWith('.g.dart')) out.push(p); + } + })(LIB); + return out; +} + +const violations = []; +for (const file of listDartFiles()) { + const lines = fs.readFileSync(file, 'utf8').split('\n'); + lines.forEach((line, i) => { + if (IGNORE.test(line)) return; // 显式豁免 + const m = HEX.exec(line) || NAMED.exec(line); + if (m) { + violations.push({ + file: path.relative(ROOT, file), + line: i + 1, + hit: m[0], + text: line.trim().slice(0, 90), + }); + } + }); +} + +const banner = (s) => `\n${'─'.repeat(60)}\n${s}\n${'─'.repeat(60)}`; +if (violations.length) { + console.log(banner(`✗ 硬编码颜色 ${violations.length} 处(应走 token,或显式 // ds-ignore: 理由)`)); + for (const v of violations) console.log(` ${v.file}:${v.line} [${v.hit}] ${v.text}`); +} else { + console.log(banner('✓ 通过:颜色全部走 token / 已显式豁免')); +} + +if (STRICT && violations.length) process.exit(1); +console.log(`\n范围:${CHANGED ? 'git 改动文件' : '全量 lib/'} 违规:${violations.length}${STRICT ? '(strict)' : '(仅报告)'}`); diff --git a/client/tool/gen_tokens.mjs b/client/tool/gen_tokens.mjs index 19eee11..c5ce733 100644 --- a/client/tool/gen_tokens.mjs +++ b/client/tool/gen_tokens.mjs @@ -128,7 +128,9 @@ const CHROME = { 'statusbar-bg':'statusbarBg','statusbar-fg':'statusbarFg','statusbar-muted':'statusbarMuted', 'statusbar-strong':'statusbarStrong','statusbar-div':'statusbarDiv','statusbar-ok':'statusbarOk', // 品牌标识色(固定不随主题) - 'brand-deep':'brandDeep','brand-mid':'brandMid','brand-wine':'brandWine', + 'brand-deep':'brandDeep','brand-mid':'brandMid','brand-wine':'brandWine','brand-dot':'brandDot', + // 彩色底上的固定前景白(横幅/徽章/品牌弹窗 chrome 前景) + 'on-accent':'onAccent','on-accent-faint':'onAccentFaint', // 遮罩 / 玻璃叠加 'scrim':'scrim','hero-glass':'heroGlass','hero-glass-strong':'heroGlassStrong', // Toast 深底