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:
@@ -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 }
|
||||
|
||||
/// 原型 .btn:h38 / 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.sm:h32 / pad 0 12 / fs-sm
|
||||
final bool large; // .btn.lg:h44 / 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(
|
||||
// disabled(onPressed=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 → 展示可点 ×(清该筛选);否则展示下拉箭头;
|
||||
// 纯切换 chip(caret=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:分段 tab(inline-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 ──────────────────────────────────────────────────────────────
|
||||
/// 原型 .searchbox:h34 / bg / border r-md / gap8 / pad 0 11,前导放大镜 14(faint)。
|
||||
/// 原型 .field:label(fs-sm muted)在控件上方、间距 6;required 缀红星。
|
||||
/// 对话框/面板表单统一用它排字段,禁止 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 / .agree(login.html / register.html):18×18 盒 + 1.5px 边 +
|
||||
/// r-sm;选中 → primary 底/边 + 白勾(13, stroke2.4);文案 fs-body,间距 8。
|
||||
/// [label] 用 Widget 以承载富文本(如注册页协议链接);[alignTop] 用于多行文案
|
||||
/// (register .agree:align-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),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 原型 .input:h38 / 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 0:M3 默认字距比原型(无字距)宽,数字/拉丁明显
|
||||
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),
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user