6238b86dcb
- 登录/注册(login.html/register.html 1:1):两栏卡片+品牌渐变面板+主题小衣服 pill(onSurface 变体)+记住我(记录并预填最近账号)+原型式 toast 校验; 登录 fidelity 1.4–2.1% 三主题全绿;注册暂不入闸(少 门店编号/兑换券 字段, 已记 CONTRACT,screens.mjs 留存根) - ds 原子补齐:DsToast(.toast 单例)/DsCheck(.check/.agree)/DsButton lg 档/ DsSelect 替换全部旧 DropdownButton/DsField label 在上/DsInput 后缀与密码形态 - 全屏统一:对话框按钮全 DsButton、盒式输入主题钉死(visualDensity.standard、 h38、InputDecorationTheme 渗漏修复)、图标全 lucide、JetBrains Mono 三端同源、 BrandMark 真相源 logo、只读模式写操作全量守卫(WriteGuard+DsToast) - 出入库列表:版式对齐原型(卡片对齐+搜索框居中)、KPI 近30天滚动、结清后 失效财务应收应付表;商品编辑抽屉介绍库改搜索下拉、图片双击全屏预览 - 删除旧 UI 死代码:DataTableCard/FormDialog/PageScaffold/SearchChip/ SelectProductDialog/tabStateProvider - golden/fidelity:stock-in/out 补注册(9%)、login 入册(8%)、goldens 全量重打; 修复 pubspec flutter_web_plugins 非法声明(CI pub get 阻断) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JJ1g8XV1YhhmHRzhwWEW7o
168 lines
7.3 KiB
JavaScript
168 lines
7.3 KiB
JavaScript
// 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',
|
||
'toast-bg':'toastBg',
|
||
};
|
||
const DARK = { a:false, b:true, c:false };
|
||
|
||
// 基础 :root{}(无 data-theme 选择器)=三主题共用默认值(如 --toast-bg),
|
||
// 主题块同名变量覆盖之。
|
||
function baseBlock(){
|
||
const m = css.match(/(?<!\])\n:root\s*\{([^}]*)\}/) || css.match(/^:root\s*\{([^}]*)\}/m);
|
||
const out = {};
|
||
if(m) for(const mm of m[1].matchAll(/--([a-z0-9-]+)\s*:\s*([^;]+);/g)) out[mm[1]] = mm[2].trim();
|
||
return out;
|
||
}
|
||
const BASE = null; // 惰性求值(css 定义在下方时避免顺序问题)
|
||
function block(key){
|
||
const m = css.match(new RegExp(`\\[data-theme="${key}"\\]\\s*\\{([^}]*)\\}`));
|
||
if(!m) throw new Error('missing theme block: '+key);
|
||
const out = baseBlock();
|
||
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');
|
||
|
||
// ── 主题无关标量(间距/圆角/字号):解析 :root{} 共享块 → AppDims ──
|
||
function rootBlock(){
|
||
// 匹配 ":root{" 而非 ":root[data-theme=...]"(前者 { 紧跟)
|
||
const m = css.match(/:root\s*\{([^}]*)\}/);
|
||
if(!m) throw new Error('missing :root shared block');
|
||
const out = {};
|
||
for(const mm of m[1].matchAll(/--([a-z0-9-]+)\s*:\s*([^;]+);/g)) out[mm[1]] = mm[2].trim();
|
||
return out;
|
||
}
|
||
// kebab → camel:sp-1→sp1 · r-sm→rSm · fs-h1→fsH1 · fs-display→fsDisplay
|
||
function camel(name){
|
||
const [h, ...rest] = name.split('-');
|
||
return h + rest.map(s => s.charAt(0).toUpperCase() + s.slice(1)).join('');
|
||
}
|
||
function px(raw){
|
||
const m = raw.trim().match(/^([\d.]+)px$/);
|
||
if(!m) throw new Error('unsupported dim value: ' + raw);
|
||
return m[1];
|
||
}
|
||
function emitDims(){
|
||
const r = rootBlock();
|
||
const groups = [
|
||
['间距', k => k.startsWith('sp-')],
|
||
['圆角', k => k.startsWith('r-')],
|
||
['字号', k => k.startsWith('fs-')],
|
||
];
|
||
const lines = [];
|
||
for(const [label, pick] of groups){
|
||
const keys = Object.keys(r).filter(pick);
|
||
if(!keys.length) continue;
|
||
lines.push(` // ${label}`);
|
||
for(const k of keys) lines.push(` static const double ${camel(k)} = ${px(r[k])};`);
|
||
}
|
||
return lines.join('\n');
|
||
}
|
||
const dimsBody = `// GENERATED by tool/gen_tokens.mjs — DO NOT EDIT.
|
||
// 源:lib/core/theme/token_source/tokens.css(:root 共享块,主题无关)。改尺寸请改源后重跑脚本。
|
||
// 间距/圆角/字号三套主题统一,故为 const 标量,不进 per-theme AppTokens。
|
||
|
||
class AppDims {
|
||
AppDims._();
|
||
${emitDims()}
|
||
}
|
||
`;
|
||
fs.writeFileSync(path.join(ROOT, 'lib/core/theme/app_dims.g.dart'), dimsBody);
|
||
console.log('generated app_dims.g.dart');
|
||
|
||
// ── 主题无关「chrome 色」::root 里固定不随主题切换的颜色 → AppChrome 常量类 ──
|
||
// 底部状态栏(深石板灰绿条)、品牌标识色(logo/字标)、遮罩/玻璃叠加、Toast 深底。
|
||
// 外壳(状态栏/品牌)要用,但不属任何主题,故不进 per-theme AppTokens。
|
||
const CHROME = {
|
||
// 状态栏(底部 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-dot':'brandDot',
|
||
// 彩色底上的固定前景白(横幅/徽章/品牌弹窗 chrome 前景)
|
||
'on-accent':'onAccent','on-accent-faint':'onAccentFaint',
|
||
// 遮罩 / 玻璃叠加
|
||
'scrim':'scrim','hero-glass':'heroGlass','hero-glass-strong':'heroGlassStrong',
|
||
// Toast 深底
|
||
'toast-bg':'toastBg',
|
||
};
|
||
function emitChrome(){
|
||
const r = rootBlock();
|
||
return Object.entries(CHROME).map(([css,dart])=>{
|
||
if(r[css]===undefined) throw new Error(`:root missing --${css}`);
|
||
return ` static const Color ${dart} = ${toColor(r[css])};`;
|
||
}).join('\n');
|
||
}
|
||
const chromeBody = `// GENERATED by tool/gen_tokens.mjs — DO NOT EDIT.
|
||
// 源:lib/core/theme/token_source/tokens.css(:root 共享块的主题无关颜色)。改色请改源后重跑脚本。
|
||
// 外壳 chrome 色:状态栏/品牌/遮罩/Toast,固定不随 A/B/C 主题切换。
|
||
import 'package:flutter/material.dart';
|
||
|
||
class AppChrome {
|
||
AppChrome._();
|
||
${emitChrome()}
|
||
}
|
||
`;
|
||
fs.writeFileSync(path.join(ROOT, 'lib/core/theme/app_chrome.g.dart'), chromeBody);
|
||
console.log('generated app_chrome.g.dart');
|