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
+358 -43
View File
@@ -3,11 +3,13 @@
// 单一组件源:尺寸/间距/圆角/字号全引 AppDims,颜色全引 context.tokens(禁硬编码)。
// 屏只组合这些组件,不再自己堆样式(见代码端真相源闸 check_ds_code)。
import 'package:flutter/material.dart';
import 'package:lucide_icons_flutter/lucide_icons.dart';
import '../../core/theme/context_tokens.dart';
import '../../core/theme/app_dims.g.dart';
import 'ds_menu.dart';
// ── .btn ──────────────────────────────────────────────────────────────────
enum DsBtnVariant { ghost, primary, danger }
enum DsBtnVariant { ghost, primary, danger, success, accent }
/// 原型 .btnh38 / pad 0 16 / r-md / fs-body / fw600 / gap7,图标 16。
/// ghost=surface+border / primary=primary+on-primary / danger=danger+白。
@@ -17,6 +19,7 @@ class DsButton extends StatelessWidget {
final VoidCallback? onPressed;
final DsBtnVariant variant;
final bool small; // .btn.smh32 / pad 0 12 / fs-sm
final bool large; // .btn.lgh44 / pad 0 22 / fs14(登录/注册主按钮)
const DsButton(
this.label, {
@@ -25,6 +28,7 @@ class DsButton extends StatelessWidget {
this.onPressed,
this.variant = DsBtnVariant.ghost,
this.small = false,
this.large = false,
});
@override
@@ -34,37 +38,53 @@ class DsButton extends StatelessWidget {
DsBtnVariant.ghost => (t.surface, t.text, t.border),
DsBtnVariant.primary => (t.primary, t.onPrimary, t.primary),
DsBtnVariant.danger => (t.danger, t.onPrimary, t.danger),
// 语义确认键(审核通过/撤回等),色取 token
DsBtnVariant.success => (t.success, t.onPrimary, t.success),
DsBtnVariant.accent => (t.accent, t.onPrimary, t.accent),
};
return Material(
color: bg,
borderRadius: BorderRadius.circular(AppDims.rMd),
child: InkWell(
onTap: onPressed,
borderRadius: BorderRadius.circular(AppDims.rMd),
child: Container(
height: small ? 32 : 38,
padding: EdgeInsets.symmetric(horizontal: small ? 12 : 16),
decoration: BoxDecoration(
// disabledonPressed=null)置灰 55%:批量替换 Material 按钮后不能丢禁用反馈
return Opacity(
opacity: onPressed == null ? 0.55 : 1,
child: Material(
color: bg,
borderRadius: BorderRadius.circular(AppDims.rMd),
child: InkWell(
onTap: onPressed,
borderRadius: BorderRadius.circular(AppDims.rMd),
border: Border.all(color: border),
child: Container(
height: small ? 32 : (large ? 44 : 38),
padding: EdgeInsets.symmetric(
horizontal: small ? 12 : (large ? 22 : 16)),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(AppDims.rMd),
border: Border.all(color: border),
),
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (icon != null) ...[
Icon(icon, size: 16, color: fg),
const SizedBox(width: 7),
],
// Flexible+ellipsis:宿主给紧约束(如 dev-card 底部 flex:1)时不溢出
Flexible(
child: Text(label,
maxLines: 1,
softWrap: false,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: fg,
fontSize: small
? AppDims.fsSm
: (large ? 14 : AppDims.fsBody),
fontWeight: FontWeight.w600)),
),
],
),
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (icon != null) ...[
Icon(icon, size: 16, color: fg),
const SizedBox(width: 7),
],
Text(label,
style: TextStyle(
color: fg,
fontSize: small ? AppDims.fsSm : AppDims.fsBody,
fontWeight: FontWeight.w600)),
],
),
),
),
);
));
}
}
@@ -91,8 +111,8 @@ class DsBadge extends StatelessWidget {
return Container(
height: 22,
padding: const EdgeInsets.symmetric(horizontal: 9),
decoration:
BoxDecoration(color: bg, borderRadius: BorderRadius.circular(AppDims.rPill)),
decoration: BoxDecoration(
color: bg, borderRadius: BorderRadius.circular(AppDims.rPill)),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
@@ -120,17 +140,23 @@ class DsChip extends StatelessWidget {
final String? value; // 选中值(.cv);非空即 on 态
final VoidCallback? onTap;
final VoidCallback? onClear; // 选中态下点 × 单独清除该筛选(不触发展开菜单)
// 纯切换 chip(原型 .chip.on,如 partners 类型筛选):selected 直接给 on 态,
// caret=false 去掉下拉箭头。
final bool selected;
final bool caret;
const DsChip(
{super.key,
required this.label,
this.value,
this.onTap,
this.onClear});
this.onClear,
this.selected = false,
this.caret = true});
@override
Widget build(BuildContext context) {
final t = context.tokens;
final on = value != null && value!.isNotEmpty;
final on = selected || (value != null && value!.isNotEmpty);
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(AppDims.rMd),
@@ -147,7 +173,7 @@ class DsChip extends StatelessWidget {
children: [
Text(label,
style: TextStyle(fontSize: AppDims.fsSm, color: t.text)),
if (on) ...[
if (value != null && value!.isNotEmpty) ...[
const SizedBox(width: 7),
Text(value!,
style: TextStyle(
@@ -155,16 +181,19 @@ class DsChip extends StatelessWidget {
color: t.primary,
fontWeight: FontWeight.w600)),
],
const SizedBox(width: 7),
// 选中且提供 onClear → 展示可点 ×(清该筛选);否则展示下拉箭头
if (on && onClear != null)
// 选中且提供 onClear → 展示可点 ×(清该筛选);否则展示下拉箭头;
// 纯切换 chipcaret=false)无尾部图标
if (on && onClear != null) ...[
const SizedBox(width: 7),
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: onClear,
child: Icon(Icons.close, size: 14, color: t.muted),
)
else
Icon(Icons.keyboard_arrow_down, size: 14, color: t.faint),
child: Icon(LucideIcons.x, size: 14, color: t.muted),
),
] else if (caret) ...[
const SizedBox(width: 7),
Icon(LucideIcons.chevronDown, size: 14, color: t.faint),
],
],
),
),
@@ -172,8 +201,290 @@ class DsChip extends StatelessWidget {
}
}
// ── .seg ─────────────────────────────────────────────────────────────────
/// 原型 .seg:分段 tabinline-flex / bg / border r-md / pad 3 gap 3)。
/// button h30 / pad 0 16 / fs-sm fw600 muted;选中=surface 底 + primary 字 + sh-1。
class DsSeg extends StatelessWidget {
final List<String> items;
final int index;
final ValueChanged<int> onChanged;
const DsSeg(
{super.key,
required this.items,
required this.index,
required this.onChanged});
@override
Widget build(BuildContext context) {
final t = context.tokens;
return Container(
padding: const EdgeInsets.all(3),
decoration: BoxDecoration(
color: t.bg,
border: Border.all(color: t.border),
borderRadius: BorderRadius.circular(AppDims.rMd),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
for (var i = 0; i < items.length; i++) ...[
if (i > 0) const SizedBox(width: 3),
InkWell(
onTap: () => onChanged(i),
borderRadius: BorderRadius.circular(AppDims.rSm),
child: Container(
height: 30,
padding: const EdgeInsets.symmetric(horizontal: 16),
alignment: Alignment.center,
decoration: BoxDecoration(
color: i == index ? t.surface : null,
borderRadius: BorderRadius.circular(AppDims.rSm),
// 原型 --sh-1: 0 1px 3px var(--shadow)
boxShadow: i == index
? [
BoxShadow(
color: t.shadow,
blurRadius: 3,
offset: const Offset(0, 1)),
]
: null,
),
child: Text(items[i],
style: TextStyle(
fontSize: AppDims.fsSm,
fontWeight: FontWeight.w600,
color: i == index ? t.primary : t.muted)),
),
),
],
],
),
);
}
}
// ── .searchbox ──────────────────────────────────────────────────────────────
/// 原型 .searchboxh34 / bg / border r-md / gap8 / pad 0 11,前导放大镜 14(faint)。
/// 原型 .fieldlabelfs-sm muted)在控件上方、间距 6required 缀红星。
/// 对话框/面板表单统一用它排字段,禁止 Material labelText 浮动标签。
class DsField extends StatelessWidget {
final String label;
final bool required;
final Widget child;
const DsField(this.label,
{super.key, this.required = false, required Widget input})
: child = input;
@override
Widget build(BuildContext context) {
final t = context.tokens;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(mainAxisSize: MainAxisSize.min, children: [
Text(label, style: TextStyle(fontSize: AppDims.fsSm, color: t.muted)),
if (required)
Text(' *',
style: TextStyle(fontSize: AppDims.fsSm, color: t.danger)),
]),
const SizedBox(height: 6),
child,
],
);
}
}
/// 原型 .check / .agreelogin.html / register.html):18×18 盒 + 1.5px 边 +
/// r-sm;选中 → primary 底/边 + 白勾(13, stroke2.4);文案 fs-body,间距 8。
/// [label] 用 Widget 以承载富文本(如注册页协议链接);[alignTop] 用于多行文案
/// register .agreealign-items:flex-start + 盒 margin-top 1)。
class DsCheck extends StatelessWidget {
final bool value;
final ValueChanged<bool> onChanged;
final Widget label;
final bool alignTop;
const DsCheck({
super.key,
required this.value,
required this.onChanged,
required this.label,
this.alignTop = false,
});
@override
Widget build(BuildContext context) {
final t = context.tokens;
return MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () => onChanged(!value),
child: Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment:
alignTop ? CrossAxisAlignment.start : CrossAxisAlignment.center,
children: [
Container(
width: 18,
height: 18,
margin:
alignTop ? const EdgeInsets.only(top: 1) : EdgeInsets.zero,
alignment: Alignment.center,
decoration: BoxDecoration(
color: value ? t.primary : t.surface,
border:
Border.all(color: value ? t.primary : t.border, width: 1.5),
borderRadius: BorderRadius.circular(AppDims.rSm),
),
child: value
? Icon(LucideIcons.check, size: 13, color: t.onPrimary)
: null,
),
SizedBox(width: alignTop ? 9 : 8),
Flexible(child: label),
],
),
),
);
}
}
/// 原型 .inputh38 / surface / 1px border / r-md / pad 0 11 / fs-body
/// focus → primary 边;readonly/disabled → bg 底 + muted 字。
class DsInput extends StatelessWidget {
final TextEditingController? controller;
final bool enabled;
final String? hintText;
final ValueChanged<String>? onChanged;
final ValueChanged<String>? onSubmitted;
final TextInputType? keyboardType;
final FocusNode? focusNode;
final bool obscureText; // 密码框
final Widget? suffix; // 行内后缀(历史下拉箭头 / 密码可见切换)
const DsInput({
super.key,
this.controller,
this.enabled = true,
this.hintText,
this.onChanged,
this.onSubmitted,
this.keyboardType,
this.focusNode,
this.obscureText = false,
this.suffix,
});
@override
Widget build(BuildContext context) {
final t = context.tokens;
OutlineInputBorder line(Color c) => OutlineInputBorder(
borderRadius: BorderRadius.circular(AppDims.rMd),
borderSide: BorderSide(color: c),
);
return SizedBox(
height: 38,
child: TextField(
controller: controller,
enabled: enabled,
onChanged: onChanged,
onSubmitted: onSubmitted,
keyboardType: keyboardType,
focusNode: focusNode,
obscureText: obscureText,
// letterSpacing 0M3 默认字距比原型(无字距)宽,数字/拉丁明显
style: TextStyle(
fontSize: AppDims.fsBody,
letterSpacing: 0,
color: enabled ? t.text : t.muted),
decoration: InputDecoration(
isDense: true,
// 装饰盒锁死 .input 的 38 高(不随 visualDensity 漂移)
constraints: const BoxConstraints.tightFor(height: 38),
filled: true,
fillColor: enabled ? t.surface : t.bg,
hintText: hintText,
hintStyle: TextStyle(fontSize: AppDims.fsBody, color: t.faint),
contentPadding:
const EdgeInsets.symmetric(horizontal: 11, vertical: 9),
suffixIcon: suffix == null
? null
: Padding(
padding: const EdgeInsets.only(right: 9), child: suffix),
suffixIconConstraints:
const BoxConstraints(minWidth: 0, minHeight: 0),
enabledBorder: line(t.border),
disabledBorder: line(t.border),
focusedBorder: line(t.primary),
border: line(t.border),
),
),
);
}
}
/// 原型 select.input 盒式外观(h38/surface/border/r-md/pad 0 11 + 右侧
/// chevron),点击弹 DsMenu.menu 弹层,含 ✓ 选中态、锚定坐标已对齐)。
/// 统一替代 Material DropdownButton——其默认灰底方角弹层不属 ds 体系。
class DsSelect<T> extends StatelessWidget {
final T? value;
final List<(T, String)> options;
final ValueChanged<T>? onChanged;
final String hint;
const DsSelect({
super.key,
required this.value,
required this.options,
required this.onChanged,
this.hint = '请选择…',
});
@override
Widget build(BuildContext context) {
final t = context.tokens;
final label =
options.where((o) => o.$1 == value).map((o) => o.$2).firstOrNull;
final enabled = onChanged != null;
return Builder(
builder: (bctx) => InkWell(
onTap: enabled
? () async {
final picked = await showDsMenu<T>(bctx, items: [
for (final o in options)
DsMenuItem(
value: o.$1, label: o.$2, selected: o.$1 == value),
]);
if (picked != null) onChanged!(picked);
}
: null,
borderRadius: BorderRadius.circular(AppDims.rMd),
child: Container(
height: 38,
padding: const EdgeInsets.symmetric(horizontal: 11),
decoration: BoxDecoration(
color: enabled ? t.surface : t.bg,
border: Border.all(color: t.border),
borderRadius: BorderRadius.circular(AppDims.rMd),
),
child: Row(children: [
Expanded(
child: Text(label ?? hint,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: AppDims.fsBody,
color: label == null
? t.faint
: (enabled ? t.text : t.muted))),
),
const SizedBox(width: 8),
Icon(LucideIcons.chevronDown, size: 14, color: t.muted),
]),
),
),
);
}
}
class DsSearchBox extends StatelessWidget {
final TextEditingController? controller;
final String hint;
@@ -204,7 +515,7 @@ class DsSearchBox extends StatelessWidget {
),
child: Row(
children: [
Icon(Icons.search, size: 14, color: t.faint),
Icon(LucideIcons.search, size: 14, color: t.faint),
const SizedBox(width: 8),
Expanded(
child: TextField(
@@ -214,7 +525,11 @@ class DsSearchBox extends StatelessWidget {
style: TextStyle(fontSize: AppDims.fsBody, color: t.text),
decoration: InputDecoration(
isCollapsed: true,
contentPadding: EdgeInsets.zero,
border: InputBorder.none,
filled: false,
enabledBorder: InputBorder.none,
focusedBorder: InputBorder.none,
hintText: hint,
hintStyle:
TextStyle(fontSize: AppDims.fsBody, color: t.faint),
@@ -236,7 +551,7 @@ class DsSearchBox extends StatelessWidget {
},
child: Padding(
padding: const EdgeInsets.only(left: 6),
child: Icon(Icons.close, size: 14, color: t.muted),
child: Icon(LucideIcons.x, size: 14, color: t.muted),
),
);
},