Files
jiu/client/lib/widgets/partner_detail_drawer.dart
T
wangjia 6238b86dcb 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
2026-07-03 09:58:14 +08:00

301 lines
11 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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,
});