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:
@@ -1,109 +1,207 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../core/theme/context_tokens.dart';
|
||||
import '../core/theme/app_tokens.dart';
|
||||
import '../core/theme/app_dims.g.dart';
|
||||
import '../core/responsive/responsive.dart';
|
||||
import '../core/utils/dialog_util.dart';
|
||||
import '../core/theme/app_fonts.dart';
|
||||
|
||||
/// 年/月/日 滚轮日期选择器(对齐原型 .wheel-pop)。选中返回 DateTime,取消返回 null。
|
||||
/// 「今天」快速跳到今天;「确定」确认。范围选择由调用方调两次(起/止)拼成。
|
||||
Future<DateTime?> showWheelDatePicker(
|
||||
BuildContext context, {
|
||||
DateTime? initial,
|
||||
String title = '选择日期',
|
||||
}) {
|
||||
return showAppDialog<DateTime>(
|
||||
context: context,
|
||||
builder: (_) => _WheelDateDialog(initial: initial ?? DateTime.now(), title: title),
|
||||
);
|
||||
}
|
||||
|
||||
class _WheelDateDialog extends StatefulWidget {
|
||||
/// 年/月/日 滚轮日期选择面板(对齐原型单一真源 .wheel-pop / datewheel.js)。
|
||||
/// 三列纯数字(年 4 位、月/日补零两位,等宽字体),中间高亮带 = .wheel-center,
|
||||
/// 居中项为 primary 加粗 16px;底部「今天」+「确定」,无取消(点外部取消)。
|
||||
///
|
||||
/// 本体是自包含的 248px 卡片(Material 描边+阴影),既可放进锚定浮层(单据/生产日期,
|
||||
/// 挂在字段下方,对齐原型 datewheel.js 的 pop 定位),也可放进居中弹窗(范围选择)。
|
||||
/// 「确定」时回调 [onCommit];「今天」仅在面板内滚动到今天,不提交。
|
||||
class WheelDatePanel extends StatefulWidget {
|
||||
final DateTime initial;
|
||||
final ValueChanged<DateTime> onCommit;
|
||||
|
||||
/// 可选标题(范围选择用「起始/结束日期」区分);空则不渲染(对齐原型无标题)。
|
||||
final String title;
|
||||
const _WheelDateDialog({required this.initial, required this.title});
|
||||
|
||||
const WheelDatePanel({
|
||||
super.key,
|
||||
required this.initial,
|
||||
required this.onCommit,
|
||||
this.title = '',
|
||||
});
|
||||
|
||||
@override
|
||||
State<_WheelDateDialog> createState() => _WheelDateDialogState();
|
||||
State<WheelDatePanel> createState() => _WheelDatePanelState();
|
||||
}
|
||||
|
||||
class _WheelDateDialogState extends State<_WheelDateDialog> {
|
||||
late DateTime _value;
|
||||
const double _kItem = 36; // .wheel-item 行高
|
||||
const double _kColsH = 180; // .wheel-cols 高度(5 行)
|
||||
const double _kWidth = 248; // .wheel-pop 宽度
|
||||
|
||||
String _pad2(int n) => n.toString().padLeft(2, '0');
|
||||
int _daysIn(int y, int m) => DateTime(y, m + 1, 0).day; // m: 1-12
|
||||
|
||||
class _WheelDatePanelState extends State<WheelDatePanel> {
|
||||
late final List<int> _years; // now-15 .. now+2(对齐 datewheel.js)
|
||||
late int _yIdx, _mIdx, _dIdx;
|
||||
late final FixedExtentScrollController _yCtrl, _mCtrl, _dCtrl;
|
||||
|
||||
int get _year => _years[_yIdx];
|
||||
int get _month => _mIdx + 1;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_value = DateTime(widget.initial.year, widget.initial.month, widget.initial.day);
|
||||
final now = DateTime.now();
|
||||
_years = [for (var y = now.year - 15; y <= now.year + 2; y++) y];
|
||||
final v = widget.initial;
|
||||
_yIdx = _years.indexOf(v.year);
|
||||
if (_yIdx < 0) _yIdx = _years.indexOf(now.year);
|
||||
if (_yIdx < 0) _yIdx = 0;
|
||||
_mIdx = (v.month - 1).clamp(0, 11);
|
||||
_dIdx = (v.day - 1).clamp(0, _daysIn(v.year, v.month) - 1);
|
||||
_yCtrl = FixedExtentScrollController(initialItem: _yIdx);
|
||||
_mCtrl = FixedExtentScrollController(initialItem: _mIdx);
|
||||
_dCtrl = FixedExtentScrollController(initialItem: _dIdx);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_yCtrl.dispose();
|
||||
_mCtrl.dispose();
|
||||
_dCtrl.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
// 年/月变化后重算当月天数,超界则回夹并吸附。
|
||||
void _onYearOrMonth() {
|
||||
final n = _daysIn(_year, _month);
|
||||
if (_dIdx > n - 1) {
|
||||
_dIdx = n - 1;
|
||||
_dCtrl.jumpToItem(_dIdx);
|
||||
}
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
void _goToday() {
|
||||
final now = DateTime.now();
|
||||
final yi = _years.indexOf(now.year);
|
||||
if (yi >= 0) _yIdx = yi;
|
||||
_mIdx = now.month - 1;
|
||||
_dIdx = now.day - 1;
|
||||
const d = Duration(milliseconds: 250);
|
||||
_yCtrl.animateToItem(_yIdx, duration: d, curve: Curves.easeOut);
|
||||
_mCtrl.animateToItem(_mIdx, duration: d, curve: Curves.easeOut);
|
||||
_dCtrl.animateToItem(_dIdx, duration: d, curve: Curves.easeOut);
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final t = context.tokens;
|
||||
return Dialog(
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(AppDims.rXl)),
|
||||
child: SizedBox(
|
||||
width: context.dialogWidth(320),
|
||||
final days = _daysIn(_year, _month);
|
||||
return Material(
|
||||
color: t.surface,
|
||||
elevation: 6,
|
||||
borderRadius: BorderRadius.circular(AppDims.rMd),
|
||||
child: Container(
|
||||
width: _kWidth,
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: t.border),
|
||||
borderRadius: BorderRadius.circular(AppDims.rMd),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 14, 16, 4),
|
||||
child: Row(
|
||||
children: [
|
||||
Text(widget.title,
|
||||
if (widget.title.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(4, 2, 4, 8),
|
||||
child: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(widget.title,
|
||||
style: TextStyle(
|
||||
fontSize: AppDims.fsTitle,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: t.heading)),
|
||||
),
|
||||
),
|
||||
// 三列滚轮 + 中间高亮带
|
||||
SizedBox(
|
||||
height: _kColsH,
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
left: 4,
|
||||
right: 4,
|
||||
top: (_kColsH - _kItem) / 2,
|
||||
height: _kItem,
|
||||
child: IgnorePointer(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: t.bg,
|
||||
borderRadius: BorderRadius.circular(AppDims.rMd),
|
||||
border: Border(
|
||||
top: BorderSide(color: t.border),
|
||||
bottom: BorderSide(color: t.border),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
_col(t, _yCtrl, _years.length,
|
||||
(i) => _years[i].toString(), _yIdx, (i) {
|
||||
_yIdx = i;
|
||||
_onYearOrMonth();
|
||||
}),
|
||||
_col(t, _mCtrl, 12, (i) => _pad2(i + 1), _mIdx, (i) {
|
||||
_mIdx = i;
|
||||
_onYearOrMonth();
|
||||
}),
|
||||
_col(t, _dCtrl, days, (i) => _pad2(i + 1), _dIdx,
|
||||
(i) => setState(() => _dIdx = i)),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// 三列滚轮(年/月/日),中间高亮行由 CupertinoDatePicker 提供。
|
||||
SizedBox(
|
||||
height: 200,
|
||||
child: CupertinoTheme(
|
||||
data: CupertinoThemeData(
|
||||
textTheme: CupertinoTextThemeData(
|
||||
dateTimePickerTextStyle: TextStyle(fontSize: 18, color: t.text),
|
||||
),
|
||||
),
|
||||
child: CupertinoDatePicker(
|
||||
mode: CupertinoDatePickerMode.date,
|
||||
initialDateTime: _value,
|
||||
minimumYear: 2015,
|
||||
maximumYear: 2035,
|
||||
onDateTimeChanged: (d) =>
|
||||
_value = DateTime(d.year, d.month, d.day),
|
||||
),
|
||||
// 底栏:今天(左) / 确定(右,无取消,点外部取消)
|
||||
Container(
|
||||
margin: const EdgeInsets.only(top: 6),
|
||||
padding: const EdgeInsets.fromLTRB(4, 8, 4, 2),
|
||||
decoration: BoxDecoration(
|
||||
border: Border(top: BorderSide(color: t.borderSubtle)),
|
||||
),
|
||||
),
|
||||
const Divider(height: 1),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(12, 8, 12, 10),
|
||||
child: Row(
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
final now = DateTime.now();
|
||||
setState(() => _value = DateTime(now.year, now.month, now.day));
|
||||
},
|
||||
child: Text('今天', style: TextStyle(color: t.muted)),
|
||||
InkWell(
|
||||
onTap: _goToday,
|
||||
borderRadius: BorderRadius.circular(AppDims.rSm),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 10, vertical: 4),
|
||||
child: Text('今天',
|
||||
style: TextStyle(
|
||||
fontSize: AppDims.fsSm, color: t.muted)),
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: Text('取消', style: TextStyle(color: t.muted)),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
ElevatedButton(
|
||||
onPressed: () => Navigator.of(context).pop(_value),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: t.primary,
|
||||
foregroundColor: t.onPrimary,
|
||||
elevation: 0,
|
||||
InkWell(
|
||||
onTap: () =>
|
||||
widget.onCommit(DateTime(_year, _month, _dIdx + 1)),
|
||||
borderRadius: BorderRadius.circular(AppDims.rSm),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16, vertical: 5),
|
||||
decoration: BoxDecoration(
|
||||
color: t.primary,
|
||||
borderRadius: BorderRadius.circular(AppDims.rSm),
|
||||
),
|
||||
child: Text('确定',
|
||||
style: TextStyle(
|
||||
fontSize: AppDims.fsSm,
|
||||
color: t.onPrimary,
|
||||
fontWeight: FontWeight.w600)),
|
||||
),
|
||||
child: const Text('确定'),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -113,6 +211,63 @@ class _WheelDateDialogState extends State<_WheelDateDialog> {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _col(
|
||||
AppTokens t,
|
||||
FixedExtentScrollController ctrl,
|
||||
int count,
|
||||
String Function(int) label,
|
||||
int selIdx,
|
||||
ValueChanged<int> onSel,
|
||||
) {
|
||||
return Expanded(
|
||||
child: ListWheelScrollView.useDelegate(
|
||||
controller: ctrl,
|
||||
itemExtent: _kItem,
|
||||
physics: const FixedExtentScrollPhysics(),
|
||||
diameterRatio: 100, // 近似平面,去掉滚轮弧度
|
||||
perspective: 0.0001,
|
||||
onSelectedItemChanged: onSel,
|
||||
childDelegate: ListWheelChildBuilderDelegate(
|
||||
childCount: count,
|
||||
builder: (c, i) {
|
||||
final on = i == selIdx;
|
||||
return Center(
|
||||
child: Text(
|
||||
label(i),
|
||||
style: TextStyle(
|
||||
fontFamily: AppFonts.mono,
|
||||
fontFamilyFallback: AppFonts.monoFallback,
|
||||
fontSize: on ? 16 : AppDims.fsBody,
|
||||
fontWeight: on ? FontWeight.w700 : FontWeight.w400,
|
||||
color: on ? t.primary : t.muted,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 居中弹窗形式(范围选择用)。锚定字段的用法见 DsDateCell(直接嵌 WheelDatePanel 到浮层)。
|
||||
Future<DateTime?> showWheelDatePicker(
|
||||
BuildContext context, {
|
||||
DateTime? initial,
|
||||
String title = '',
|
||||
}) {
|
||||
return showAppDialog<DateTime>(
|
||||
context: context,
|
||||
builder: (dctx) => Align(
|
||||
alignment: Alignment.center,
|
||||
child: WheelDatePanel(
|
||||
initial: initial ?? DateTime.now(),
|
||||
title: title,
|
||||
onCommit: (d) => Navigator.of(dctx).pop(d),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 范围选择:先选起始日、再选结束日;任一取消则返回 null。
|
||||
@@ -126,7 +281,6 @@ Future<DateTimeRange?> showWheelDateRange(
|
||||
final end = await showWheelDatePicker(context,
|
||||
initial: initial?.end ?? start, title: '结束日期');
|
||||
if (end == null) return null;
|
||||
// 保证 start <= end
|
||||
return end.isBefore(start)
|
||||
? DateTimeRange(start: end, end: start)
|
||||
: DateTimeRange(start: start, end: end);
|
||||
|
||||
Reference in New Issue
Block a user