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
@@ -0,0 +1,300 @@
// widgets/partner_detail_drawer.dart — 往来单位详情抽屉(镜像原型 partners.html openDetail)。
// 结构照 atoms .drawer/.dsec/.drow/.dli:联系信息 / 应收应付(应收染 success、应付染 accent
// / 近期单据(财务流水,已覆盖入库应付、出库应收与收付款)/ 底部 对账单+编辑。
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:intl/intl.dart';
import 'package:lucide_icons_flutter/lucide_icons.dart';
import '../core/theme/app_chrome.g.dart';
import '../core/theme/app_dims.g.dart';
import '../core/theme/context_tokens.dart';
import '../models/finance.dart';
import '../models/partner.dart';
import '../providers/partner_provider.dart';
import 'ds/ds_atoms.dart';
import 'write_guard.dart';
import '../core/theme/app_fonts.dart';
import 'ds/ds_toast.dart';
/// 打开往来单位详情抽屉。应收/应付由列表页的 partnerBalancesProvider 传入;
/// 近期单据抽屉内经 partnerRecentDocsProvider 拉取。
Future<void> showPartnerDetailDrawer(
BuildContext context, {
required Partner partner,
({double recv, double pay})? balance,
VoidCallback? onEdit,
}) {
return showGeneralDialog<void>(
context: context,
useRootNavigator: false,
barrierDismissible: true,
barrierLabel: '关闭',
barrierColor: AppChrome.scrim, // .drawer-mask --scrim
transitionDuration: const Duration(milliseconds: 250),
pageBuilder: (ctx, _, __) {
final w = math.min(480.0, MediaQuery.of(ctx).size.width * 0.94);
return Align(
alignment: Alignment.centerRight,
child: Material(
color: ctx.tokens.surface,
elevation: 16,
child: SelectionArea(
child: SizedBox(
width: w,
height: double.infinity,
child: _PartnerDetailDrawer(
partner: partner, balance: balance, onEdit: onEdit),
),
),
),
);
},
transitionBuilder: (ctx, anim, _, child) => SlideTransition(
position: Tween<Offset>(begin: const Offset(1, 0), end: Offset.zero)
.animate(CurvedAnimation(parent: anim, curve: Curves.easeOutCubic)),
child: child,
),
);
}
/// 金额:原型预格式化字符串风格 ¥x,xxx(整数无小数,非整保留两位)。
String fmtYuan(double v) {
final money = v == v.roundToDouble()
? NumberFormat.decimalPattern().format(v.round())
: NumberFormat('#,##0.00').format(v);
return '¥$money';
}
class _PartnerDetailDrawer extends ConsumerWidget {
final Partner partner;
final ({double recv, double pay})? balance;
final VoidCallback? onEdit;
const _PartnerDetailDrawer(
{required this.partner, this.balance, this.onEdit});
@override
Widget build(BuildContext context, WidgetRef ref) {
final t = context.tokens;
final docs = ref.watch(partnerRecentDocsProvider(partner.id));
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// ── .drawer-head:名称 + X ──
Container(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 18),
decoration: BoxDecoration(
border: Border(bottom: BorderSide(color: t.border))),
child: Row(
children: [
Expanded(
child: Text(partner.name,
style: TextStyle(
fontSize: AppDims.fsH2,
fontWeight: FontWeight.w700,
color: t.heading)),
),
IconButton(
onPressed: () => Navigator.of(context).pop(),
icon: Icon(LucideIcons.x, size: 18, color: t.muted),
tooltip: '关闭',
),
],
),
),
// ── .drawer-body ──
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 18),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_dsec(t, '联系信息', first: true),
_drow(t, '类型', badge: partnerTypeBadge(partner)),
_drow(t, '联系人',
text: (partner.contact?.isNotEmpty == true)
? partner.contact!
: ''),
_drow(t, '电话',
text: (partner.phone?.isNotEmpty == true)
? partner.phone!
: '',
mono: true),
_drow(t, '地址',
text: (partner.address?.isNotEmpty == true)
? partner.address!
: '',
wrap: true),
_dsec(t, '应收 / 应付'),
_drow(t, '应收账款',
text: fmtYuan(balance?.recv ?? 0),
mono: true,
color: t.success),
_drow(t, '应付账款',
text: fmtYuan(balance?.pay ?? 0),
mono: true,
color: t.accent),
_dsec(t, '近期单据'),
docs.when(
loading: () => const Padding(
padding: EdgeInsets.all(20),
child: Center(
child: SizedBox(
width: 18,
height: 18,
child: CircularProgressIndicator(strokeWidth: 2))),
),
error: (e, _) => Padding(
padding: const EdgeInsets.symmetric(vertical: 12),
child: Text('单据加载失败',
style:
TextStyle(color: t.muted, fontSize: AppDims.fsSm)),
),
data: (rows) => rows.isEmpty
? Padding(
padding: const EdgeInsets.symmetric(vertical: 12),
child: Text('暂无往来单据',
style: TextStyle(
color: t.muted, fontSize: AppDims.fsSm)),
)
: Column(children: [for (final r in rows) _dli(t, r)]),
),
],
),
),
),
// ── 底部操作(原型:对账单 / 编辑)──
Container(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 14),
decoration:
BoxDecoration(border: Border(top: BorderSide(color: t.border))),
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
DsButton('对账单', small: true, onPressed: () {
showDsToast(context, '对账单功能即将上线');
}),
const SizedBox(width: 10),
if (onEdit != null)
WriteGuard(
child: DsButton('编辑', small: true, onPressed: () {
Navigator.of(context).pop();
onEdit!();
}),
),
],
),
),
],
);
}
/// 原型 .dsec:区块小标题(fs-xs muted 600,上距 18)。
Widget _dsec(dynamic t, String label, {bool first = false}) => Padding(
padding: EdgeInsets.only(top: first ? 0 : 18, bottom: 4),
child: Align(
alignment: Alignment.centerLeft,
child: Text(label,
style: TextStyle(
fontSize: AppDims.fsXs,
color: t.muted,
fontWeight: FontWeight.w600,
letterSpacing: .4)),
),
);
/// 原型 .drowlabel(muted) + b(text 600)pad 11 0,下边 border-subtle。
Widget _drow(dynamic t, String label,
{String? text,
Widget? badge,
bool mono = false,
bool wrap = false,
Color? color}) {
return Container(
padding: const EdgeInsets.symmetric(vertical: 11),
decoration: BoxDecoration(
border: Border(bottom: BorderSide(color: t.borderSubtle))),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(label,
style: TextStyle(fontSize: AppDims.fsBody, color: t.muted)),
const Spacer(),
if (badge != null)
badge
else
ConstrainedBox(
// 原型地址行 max-width:200 右对齐显示完整地址
constraints: BoxConstraints(maxWidth: wrap ? 200 : 260),
child: Text(text ?? '',
textAlign: TextAlign.right,
style: TextStyle(
fontSize: AppDims.fsBody,
fontWeight: FontWeight.w600,
color: color ?? t.text,
fontFamily: mono ? 'monospace' : null)),
),
],
),
);
}
/// 原型 .dli:近期单据行(左 标题+small 日期,右 金额 mono 600)。
Widget _dli(dynamic t, FinanceRecord r) {
// 收款/付款是清账方向,按原型示例带负号
final out = r.type == 'receipt' || r.type == 'payment';
return Container(
padding: const EdgeInsets.symmetric(vertical: 10),
decoration: BoxDecoration(
border: Border(bottom: BorderSide(color: t.borderSubtle))),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(r.docTitle,
style: TextStyle(fontSize: AppDims.fsSm, color: t.text)),
Padding(
padding: const EdgeInsets.only(top: 2),
child: Text(
[
if ((r.recordDate ?? '').isNotEmpty)
r.recordDate!.split('T').first,
if ((r.remark ?? '').isNotEmpty) r.remark!,
].join(' · '),
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: AppDims.fsXs,
color: t.faint,
fontFamily: AppFonts.mono,
fontFamilyFallback: AppFonts.monoFallback)),
),
],
),
),
const SizedBox(width: 12),
Text('${out ? '-' : ''}${fmtYuan(r.amount)}',
style: TextStyle(
fontSize: AppDims.fsSm,
fontWeight: FontWeight.w600,
color: t.heading,
fontFamily: AppFonts.mono,
fontFamilyFallback: AppFonts.monoFallback)),
],
),
);
}
}
/// 类型徽章(原型 b-供应商=info / b-客户=accent / b-两者=warn),列表/卡片/抽屉共用。
DsBadge partnerTypeBadge(Partner p) => DsBadge(p.typeLabel,
tone: switch (p.typeLabel) {
'两者' => DsBadgeTone.warn,
'客户' => DsBadgeTone.accent,
_ => DsBadgeTone.info,
});