feat(tools): 代码端真相源闸 check_ds_code + 外壳硬编码颜色清零
把原型 check-ds 的「单一真源」纪律延伸到 Flutter 代码: - client/tool/check_ds_code.mjs:扫 lib/(排除.g.dart) 硬编码 Color(0x..)/Colors.具名色, --strict/--changed 报错挡提交,合理特例须显式 // ds-ignore: 理由 - tokens.css(单源,client+原型同步) 加 chrome 前景色 --brand-dot/--on-accent/--on-accent-faint, codegen 进 AppChrome;外壳 14 处硬编码(白字/品牌点/grace橙)全部改走 token - 外壳+3 新 widget 现 0 违规;全量存量 172→158(其余屏后续逐步清) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TSKEiHsvauyxYUW2itzUXX
This commit is contained in:
@@ -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 深底(主题无关) */
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 深底(主题无关) */
|
||||
|
||||
@@ -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<AppShell> {
|
||||
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<AppShell> {
|
||||
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<AppShell> {
|
||||
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<AppShell> {
|
||||
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<AppShell> {
|
||||
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<AppShell> {
|
||||
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(),
|
||||
),
|
||||
|
||||
@@ -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)' : '(仅报告)'}`);
|
||||
@@ -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 深底
|
||||
|
||||
Reference in New Issue
Block a user