6238b86dcb
- 登录/注册(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
301 lines
11 KiB
Dart
301 lines
11 KiB
Dart
// 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,
|
||
});
|