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:
@@ -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)),
|
||||
),
|
||||
);
|
||||
|
||||
/// 原型 .drow:label(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,
|
||||
});
|
||||
Reference in New Issue
Block a user