feat(client): 登录/注册页照原型重建,ds 真相源组件族统一全部屏

- 登录/注册(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
This commit is contained in:
wangjia
2026-07-03 09:58:14 +08:00
parent ca7595b113
commit 6238b86dcb
286 changed files with 16831 additions and 11136 deletions
+19
View File
@@ -0,0 +1,19 @@
/// 字体真相源。
///
/// 等宽字体镜像原型 tokens.css `--font-mono` 首选 **JetBrains Mono**
/// 以资产打包(pubspec `JetBrainsMono` 400/700)——原型截图、golden 测试、
/// 三端真机渲染完全同源。历史教训:曾写 CSS 泛型名 `'monospace'`Flutter
/// **不解析**并静默回退(真机无衬线、golden 里 tofu 方块),勿走回头路。
/// CJK 字形不在 JetBrains Mono 内,回退链兜底系统等宽/默认字体。
class AppFonts {
const AppFonts._();
static const String mono = 'JetBrainsMono';
static const List<String> monoFallback = [
'Menlo',
'SF Mono',
'Consolas',
'Roboto Mono',
'monospace',
];
}
+221 -60
View File
@@ -2,61 +2,193 @@ import 'package:flutter/material.dart';
@immutable
class AppTokens extends ThemeExtension<AppTokens> {
final Color page, bg, surface, border, borderSubtle, text, heading, title, muted, faint;
final Color page,
bg,
surface,
border,
borderSubtle,
text,
heading,
title,
muted,
faint;
final Color primary, primaryDark, onPrimary, brand400, brand50, accent;
final Color success, danger, warn, infoSoft, okSoft, accentSoft, successBg, dangerBg, warnBg;
final Color success,
danger,
warn,
infoSoft,
okSoft,
accentSoft,
successBg,
dangerBg,
warnBg;
final Color thBg, rowHover, shadow;
final Color topBg, topFg, topFaint, topBorder, topControl, topMarkBg, topMarkFg;
final Color sideBg, sideFg, sideMuted, sideActiveBg, sideActiveFg, sideBorder, sideHover;
final Color topBg,
topFg,
topFaint,
topBorder,
topControl,
topMarkBg,
topMarkFg;
final Color sideBg,
sideFg,
sideMuted,
sideActiveBg,
sideActiveFg,
sideBorder,
sideHover;
final Color menuUserBg, menuUserFg, menuUserHover;
final Color toastBg; // 原型 --toast-bg(基础 :root 共用,主题不变)
final bool isDark;
const AppTokens({
required this.page, required this.bg, required this.surface, required this.border,
required this.borderSubtle, required this.text, required this.heading, required this.title,
required this.muted, required this.faint, required this.primary, required this.primaryDark,
required this.onPrimary, required this.brand400, required this.brand50, required this.accent,
required this.success, required this.danger, required this.warn, required this.infoSoft,
required this.okSoft, required this.accentSoft, required this.successBg, required this.dangerBg,
required this.warnBg, required this.thBg, required this.rowHover, required this.shadow,
required this.topBg, required this.topFg, required this.topFaint, required this.topBorder,
required this.topControl, required this.topMarkBg, required this.topMarkFg, required this.sideBg,
required this.sideFg, required this.sideMuted, required this.sideActiveBg, required this.sideActiveFg,
required this.sideBorder, required this.sideHover, required this.menuUserBg, required this.menuUserFg,
required this.menuUserHover, required this.isDark,
required this.page,
required this.bg,
required this.surface,
required this.border,
required this.borderSubtle,
required this.text,
required this.heading,
required this.title,
required this.muted,
required this.faint,
required this.primary,
required this.primaryDark,
required this.onPrimary,
required this.brand400,
required this.brand50,
required this.accent,
required this.success,
required this.danger,
required this.warn,
required this.infoSoft,
required this.okSoft,
required this.accentSoft,
required this.successBg,
required this.dangerBg,
required this.warnBg,
required this.thBg,
required this.rowHover,
required this.shadow,
required this.topBg,
required this.topFg,
required this.topFaint,
required this.topBorder,
required this.topControl,
required this.topMarkBg,
required this.topMarkFg,
required this.sideBg,
required this.sideFg,
required this.sideMuted,
required this.sideActiveBg,
required this.sideActiveFg,
required this.sideBorder,
required this.sideHover,
required this.menuUserBg,
required this.menuUserFg,
required this.menuUserHover,
required this.toastBg,
required this.isDark,
});
@override
AppTokens copyWith({
Color? page, Color? bg, Color? surface, Color? border, Color? borderSubtle, Color? text,
Color? heading, Color? title, Color? muted, Color? faint, Color? primary, Color? primaryDark,
Color? onPrimary, Color? brand400, Color? brand50, Color? accent, Color? success, Color? danger,
Color? warn, Color? infoSoft, Color? okSoft, Color? accentSoft, Color? successBg, Color? dangerBg,
Color? warnBg, Color? thBg, Color? rowHover, Color? shadow, Color? topBg, Color? topFg,
Color? topFaint, Color? topBorder, Color? topControl, Color? topMarkBg, Color? topMarkFg,
Color? sideBg, Color? sideFg, Color? sideMuted, Color? sideActiveBg, Color? sideActiveFg,
Color? sideBorder, Color? sideHover, Color? menuUserBg, Color? menuUserFg, Color? menuUserHover,
Color? page,
Color? bg,
Color? surface,
Color? border,
Color? borderSubtle,
Color? text,
Color? heading,
Color? title,
Color? muted,
Color? faint,
Color? primary,
Color? primaryDark,
Color? onPrimary,
Color? brand400,
Color? brand50,
Color? accent,
Color? success,
Color? danger,
Color? warn,
Color? infoSoft,
Color? okSoft,
Color? accentSoft,
Color? successBg,
Color? dangerBg,
Color? warnBg,
Color? thBg,
Color? rowHover,
Color? shadow,
Color? topBg,
Color? topFg,
Color? topFaint,
Color? topBorder,
Color? topControl,
Color? topMarkBg,
Color? topMarkFg,
Color? sideBg,
Color? sideFg,
Color? sideMuted,
Color? sideActiveBg,
Color? sideActiveFg,
Color? sideBorder,
Color? sideHover,
Color? menuUserBg,
Color? menuUserFg,
Color? menuUserHover,
Color? toastBg,
bool? isDark,
}) {
return AppTokens(
page: page ?? this.page, bg: bg ?? this.bg, surface: surface ?? this.surface,
border: border ?? this.border, borderSubtle: borderSubtle ?? this.borderSubtle,
text: text ?? this.text, heading: heading ?? this.heading, title: title ?? this.title,
muted: muted ?? this.muted, faint: faint ?? this.faint, primary: primary ?? this.primary,
primaryDark: primaryDark ?? this.primaryDark, onPrimary: onPrimary ?? this.onPrimary,
brand400: brand400 ?? this.brand400, brand50: brand50 ?? this.brand50, accent: accent ?? this.accent,
success: success ?? this.success, danger: danger ?? this.danger, warn: warn ?? this.warn,
infoSoft: infoSoft ?? this.infoSoft, okSoft: okSoft ?? this.okSoft, accentSoft: accentSoft ?? this.accentSoft,
successBg: successBg ?? this.successBg, dangerBg: dangerBg ?? this.dangerBg, warnBg: warnBg ?? this.warnBg,
thBg: thBg ?? this.thBg, rowHover: rowHover ?? this.rowHover, shadow: shadow ?? this.shadow,
topBg: topBg ?? this.topBg, topFg: topFg ?? this.topFg, topFaint: topFaint ?? this.topFaint,
topBorder: topBorder ?? this.topBorder, topControl: topControl ?? this.topControl,
topMarkBg: topMarkBg ?? this.topMarkBg, topMarkFg: topMarkFg ?? this.topMarkFg,
sideBg: sideBg ?? this.sideBg, sideFg: sideFg ?? this.sideFg, sideMuted: sideMuted ?? this.sideMuted,
sideActiveBg: sideActiveBg ?? this.sideActiveBg, sideActiveFg: sideActiveFg ?? this.sideActiveFg,
sideBorder: sideBorder ?? this.sideBorder, sideHover: sideHover ?? this.sideHover,
menuUserBg: menuUserBg ?? this.menuUserBg, menuUserFg: menuUserFg ?? this.menuUserFg,
menuUserHover: menuUserHover ?? this.menuUserHover, isDark: isDark ?? this.isDark,
page: page ?? this.page,
bg: bg ?? this.bg,
surface: surface ?? this.surface,
border: border ?? this.border,
borderSubtle: borderSubtle ?? this.borderSubtle,
text: text ?? this.text,
heading: heading ?? this.heading,
title: title ?? this.title,
muted: muted ?? this.muted,
faint: faint ?? this.faint,
primary: primary ?? this.primary,
primaryDark: primaryDark ?? this.primaryDark,
onPrimary: onPrimary ?? this.onPrimary,
brand400: brand400 ?? this.brand400,
brand50: brand50 ?? this.brand50,
accent: accent ?? this.accent,
success: success ?? this.success,
danger: danger ?? this.danger,
warn: warn ?? this.warn,
infoSoft: infoSoft ?? this.infoSoft,
okSoft: okSoft ?? this.okSoft,
accentSoft: accentSoft ?? this.accentSoft,
successBg: successBg ?? this.successBg,
dangerBg: dangerBg ?? this.dangerBg,
warnBg: warnBg ?? this.warnBg,
thBg: thBg ?? this.thBg,
rowHover: rowHover ?? this.rowHover,
shadow: shadow ?? this.shadow,
topBg: topBg ?? this.topBg,
topFg: topFg ?? this.topFg,
topFaint: topFaint ?? this.topFaint,
topBorder: topBorder ?? this.topBorder,
topControl: topControl ?? this.topControl,
topMarkBg: topMarkBg ?? this.topMarkBg,
topMarkFg: topMarkFg ?? this.topMarkFg,
sideBg: sideBg ?? this.sideBg,
sideFg: sideFg ?? this.sideFg,
sideMuted: sideMuted ?? this.sideMuted,
sideActiveBg: sideActiveBg ?? this.sideActiveBg,
sideActiveFg: sideActiveFg ?? this.sideActiveFg,
sideBorder: sideBorder ?? this.sideBorder,
sideHover: sideHover ?? this.sideHover,
menuUserBg: menuUserBg ?? this.menuUserBg,
menuUserFg: menuUserFg ?? this.menuUserFg,
menuUserHover: menuUserHover ?? this.menuUserHover,
toastBg: toastBg ?? this.toastBg,
isDark: isDark ?? this.isDark,
);
}
@@ -65,24 +197,53 @@ class AppTokens extends ThemeExtension<AppTokens> {
if (other is! AppTokens) return this;
Color c(Color a, Color b) => Color.lerp(a, b, t)!;
return AppTokens(
page: c(page, other.page), bg: c(bg, other.bg), surface: c(surface, other.surface),
border: c(border, other.border), borderSubtle: c(borderSubtle, other.borderSubtle),
text: c(text, other.text), heading: c(heading, other.heading), title: c(title, other.title),
muted: c(muted, other.muted), faint: c(faint, other.faint), primary: c(primary, other.primary),
primaryDark: c(primaryDark, other.primaryDark), onPrimary: c(onPrimary, other.onPrimary),
brand400: c(brand400, other.brand400), brand50: c(brand50, other.brand50), accent: c(accent, other.accent),
success: c(success, other.success), danger: c(danger, other.danger), warn: c(warn, other.warn),
infoSoft: c(infoSoft, other.infoSoft), okSoft: c(okSoft, other.okSoft), accentSoft: c(accentSoft, other.accentSoft),
successBg: c(successBg, other.successBg), dangerBg: c(dangerBg, other.dangerBg), warnBg: c(warnBg, other.warnBg),
thBg: c(thBg, other.thBg), rowHover: c(rowHover, other.rowHover), shadow: c(shadow, other.shadow),
topBg: c(topBg, other.topBg), topFg: c(topFg, other.topFg), topFaint: c(topFaint, other.topFaint),
topBorder: c(topBorder, other.topBorder), topControl: c(topControl, other.topControl),
topMarkBg: c(topMarkBg, other.topMarkBg), topMarkFg: c(topMarkFg, other.topMarkFg),
sideBg: c(sideBg, other.sideBg), sideFg: c(sideFg, other.sideFg), sideMuted: c(sideMuted, other.sideMuted),
sideActiveBg: c(sideActiveBg, other.sideActiveBg), sideActiveFg: c(sideActiveFg, other.sideActiveFg),
sideBorder: c(sideBorder, other.sideBorder), sideHover: c(sideHover, other.sideHover),
menuUserBg: c(menuUserBg, other.menuUserBg), menuUserFg: c(menuUserFg, other.menuUserFg),
menuUserHover: c(menuUserHover, other.menuUserHover), isDark: t < 0.5 ? isDark : other.isDark,
page: c(page, other.page),
bg: c(bg, other.bg),
surface: c(surface, other.surface),
border: c(border, other.border),
borderSubtle: c(borderSubtle, other.borderSubtle),
text: c(text, other.text),
heading: c(heading, other.heading),
title: c(title, other.title),
muted: c(muted, other.muted),
faint: c(faint, other.faint),
primary: c(primary, other.primary),
primaryDark: c(primaryDark, other.primaryDark),
onPrimary: c(onPrimary, other.onPrimary),
brand400: c(brand400, other.brand400),
brand50: c(brand50, other.brand50),
accent: c(accent, other.accent),
success: c(success, other.success),
danger: c(danger, other.danger),
warn: c(warn, other.warn),
infoSoft: c(infoSoft, other.infoSoft),
okSoft: c(okSoft, other.okSoft),
accentSoft: c(accentSoft, other.accentSoft),
successBg: c(successBg, other.successBg),
dangerBg: c(dangerBg, other.dangerBg),
warnBg: c(warnBg, other.warnBg),
thBg: c(thBg, other.thBg),
rowHover: c(rowHover, other.rowHover),
shadow: c(shadow, other.shadow),
topBg: c(topBg, other.topBg),
topFg: c(topFg, other.topFg),
topFaint: c(topFaint, other.topFaint),
topBorder: c(topBorder, other.topBorder),
topControl: c(topControl, other.topControl),
topMarkBg: c(topMarkBg, other.topMarkBg),
topMarkFg: c(topMarkFg, other.topMarkFg),
sideBg: c(sideBg, other.sideBg),
sideFg: c(sideFg, other.sideFg),
sideMuted: c(sideMuted, other.sideMuted),
sideActiveBg: c(sideActiveBg, other.sideActiveBg),
sideActiveFg: c(sideActiveFg, other.sideActiveFg),
sideBorder: c(sideBorder, other.sideBorder),
sideHover: c(sideHover, other.sideHover),
menuUserBg: c(menuUserBg, other.menuUserBg),
menuUserFg: c(menuUserFg, other.menuUserFg),
menuUserHover: c(menuUserHover, other.menuUserHover),
toastBg: c(toastBg, other.toastBg),
isDark: t < 0.5 ? isDark : other.isDark,
);
}
}
+9 -3
View File
@@ -49,6 +49,7 @@ const AppTokens kTokensA = AppTokens(
menuUserBg: Color(0xFF3C79C0),
menuUserFg: Color(0xFFFFFFFF),
menuUserHover: Color(0xFF4D88CC),
toastBg: Color(0xFF1F2330),
isDark: false,
);
@@ -98,6 +99,7 @@ const AppTokens kTokensB = AppTokens(
menuUserBg: Color(0xFF3A3120),
menuUserFg: Color(0xFFF0E7D6),
menuUserHover: Color(0xFF473B27),
toastBg: Color(0xFF1F2330),
isDark: true,
);
@@ -147,13 +149,17 @@ const AppTokens kTokensC = AppTokens(
menuUserBg: Color(0xFF7E2B3B),
menuUserFg: Color(0xFFF7E9D6),
menuUserHover: Color(0xFF8E3344),
toastBg: Color(0xFF1F2330),
isDark: false,
);
AppTokens appTokensOf(String key) {
switch (key) {
case 'b': return kTokensB;
case 'c': return kTokensC;
default: return kTokensA;
case 'b':
return kTokensB;
case 'c':
return kTokensC;
default:
return kTokensA;
}
}
+83
View File
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'app_dims.g.dart';
import 'app_tokens.g.dart';
ThemeData buildTheme(String key) {
@@ -18,6 +19,88 @@ ThemeData buildTheme(String key) {
brightness: brightness,
colorScheme: scheme,
scaffoldBackgroundColor: t.bg,
// 桌面端默认 adaptivePlatformDensity=compact 会把输入框/按钮压矮 8px,
// 与原型 px 规格(.input h38 等)和 golden(测试环境=standard)不一致——
// 真机曾因此输入框只有 ~30 高。锁 standard,三端渲染与原型同几何。
visualDensity: VisualDensity.standard,
// ── 对话框/输入框/按钮的 Material 默认样式钉到 ds 真相源 ──
// 原型 .modalsurface / border / r-xl / 标题 fs-h2 heading
dialogTheme: DialogThemeData(
backgroundColor: t.surface,
surfaceTintColor: Colors.transparent,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppDims.rXl),
side: BorderSide(color: t.border),
),
titleTextStyle: TextStyle(
fontSize: AppDims.fsH2,
fontWeight: FontWeight.w700,
color: t.heading),
contentTextStyle: TextStyle(fontSize: AppDims.fsBody, color: t.text),
),
// 原型 .inputh38 盒式(surface/1px border/r-md/pad 0 11),聚焦主色边。
// 全局兜底——未显式给 decoration 边框的 TextField/DropdownButtonFormField
// 一律盒式,禁止 Material 默认下划线再出现。
inputDecorationTheme: InputDecorationTheme(
isDense: true,
filled: true,
fillColor: t.surface,
hintStyle: TextStyle(fontSize: AppDims.fsBody, color: t.faint),
labelStyle: TextStyle(fontSize: AppDims.fsSm, color: t.muted),
contentPadding: const EdgeInsets.symmetric(horizontal: 11, vertical: 9),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(AppDims.rMd),
borderSide: BorderSide(color: t.border),
),
disabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(AppDims.rMd),
borderSide: BorderSide(color: t.border),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(AppDims.rMd),
borderSide: BorderSide(color: t.primary),
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(AppDims.rMd),
borderSide: BorderSide(color: t.border),
),
),
// 原型 .btn.primaryprimary 底 / onPrimary 字 / h38 / r-md / w600 / 无阴影
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
backgroundColor: t.primary,
foregroundColor: t.onPrimary,
elevation: 0,
minimumSize: const Size(0, 38),
padding: const EdgeInsets.symmetric(horizontal: 16),
textStyle: const TextStyle(
fontSize: AppDims.fsBody, fontWeight: FontWeight.w600),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppDims.rMd)),
),
),
filledButtonTheme: FilledButtonThemeData(
style: FilledButton.styleFrom(
backgroundColor: t.primary,
foregroundColor: t.onPrimary,
minimumSize: const Size(0, 38),
padding: const EdgeInsets.symmetric(horizontal: 16),
textStyle: const TextStyle(
fontSize: AppDims.fsBody, fontWeight: FontWeight.w600),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppDims.rMd)),
),
),
// TextButton 兼作行内链接,不加边框;只统一字号字重与圆角
textButtonTheme: TextButtonThemeData(
style: TextButton.styleFrom(
minimumSize: const Size(0, 38),
textStyle: const TextStyle(
fontSize: AppDims.fsBody, fontWeight: FontWeight.w600),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppDims.rMd)),
),
),
// 字体走系统默认(macOS/iOS=PingFang SC,与原型 --font 一致);不强制 Noto。
// Noto 仅 golden 测试加载(CI 无中文字体),见 test/support/golden_harness.dart。
extensions: <ThemeExtension<dynamic>>[t],