4287f5a876
- 往来单位:窄屏隐藏页内大标题(标题在壳顶栏);MSearchRow 搜索行; 类型 pills 全部/供应商/客户/两者(两者=客户端过滤,服务端 FIND_IN_SET 无独立值); 卡片流类型+状态徽章换图标变体、mc-sub mono 编码 - 基础数据:窄屏隐藏大标题;各 tab 搜索行换 MSearchRow(清空 × 并入,重置钮撤); 档案/字典卡片副行 mono 编码;系列/规格/仓库 新增编辑窄屏走 showMSheet 底部 sheet 轻表单 - 商品详情:窄屏 padding 收紧 sp4;信息卡 Wrap→单列;图片缩略图行窄屏横滚防溢出; 二维码弹窗宽度走 context.dialogWidth - golden:新增 m_partners / m_products / m_products_series(390×844 @2x 三主题); 重生成既有 partners_mobile / products_archive_mobile 基准(桌面 golden 零漂移) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
653 lines
24 KiB
Dart
653 lines
24 KiB
Dart
// screens/partners/partners_screen.dart — 往来单位(照原型 partners.html 1:1 重建)。
|
||
// 版式:.head(标题+副标+刷新/新增) + .toolbar(搜索 260 + 类型 chips + 重置)
|
||
// + .table 7 列(名称双行/类型徽章/联系人/电话/应收/应付/操作) + .pager 仅文案。
|
||
// 行点击 → 详情抽屉(partner_detail_drawer);新增/编辑 → 弹窗(原型 frow 布局)。
|
||
import 'dart:async';
|
||
|
||
import 'package:flutter/material.dart';
|
||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
||
|
||
import '../../core/config/app_constants.dart';
|
||
import '../../core/responsive/responsive.dart';
|
||
import '../../core/theme/app_dims.g.dart';
|
||
import '../../core/theme/context_tokens.dart';
|
||
import '../../core/utils/dialog_util.dart';
|
||
import '../../core/utils/export_util.dart';
|
||
import '../../models/partner.dart';
|
||
import '../../providers/partner_provider.dart';
|
||
import '../../widgets/ds/ds_atoms.dart';
|
||
import '../../widgets/ds/ds_table.dart';
|
||
import '../../widgets/ds/m_search_row.dart';
|
||
import '../../widgets/ds/status_icon_map.dart';
|
||
import '../../widgets/mobile_list_card.dart';
|
||
import '../../widgets/partner_detail_drawer.dart';
|
||
import '../../widgets/write_guard.dart';
|
||
import '../../core/theme/app_fonts.dart';
|
||
import '../../widgets/ds/ds_toast.dart';
|
||
|
||
class PartnersScreen extends ConsumerStatefulWidget {
|
||
const PartnersScreen({super.key});
|
||
|
||
@override
|
||
ConsumerState<PartnersScreen> createState() => _PartnersScreenState();
|
||
}
|
||
|
||
class _PartnersScreenState extends ConsumerState<PartnersScreen> {
|
||
final _searchCtrl = TextEditingController();
|
||
Timer? _debounce;
|
||
// 类型 chips(原型 TYPES 筛选):全部 / 供应商 / 客户;「两者」在两侧均出现
|
||
String _typeChip = '全部';
|
||
|
||
static const _typeChips = ['全部', '供应商', '客户'];
|
||
// 窄屏对齐 m-partners 原型 F=['全部','供应商','客户','两者'];
|
||
// 「两者」后端无独立 type 值(FIND_IN_SET 语义),列表本就一页取全 → 客户端过滤。
|
||
static const _typeChipsMobile = ['全部', '供应商', '客户', '两者'];
|
||
|
||
@override
|
||
void dispose() {
|
||
_debounce?.cancel();
|
||
_searchCtrl.dispose();
|
||
super.dispose();
|
||
}
|
||
|
||
void _setType(String chip) {
|
||
setState(() => _typeChip = chip);
|
||
ref.read(partnersScreenProvider.notifier).setType(switch (chip) {
|
||
'供应商' => 'supplier',
|
||
'客户' => 'customer',
|
||
_ => null,
|
||
});
|
||
}
|
||
|
||
void _clearFilters() {
|
||
_searchCtrl.clear();
|
||
setState(() => _typeChip = '全部');
|
||
final n = ref.read(partnersScreenProvider.notifier);
|
||
n.setKeyword('');
|
||
n.setType(null);
|
||
}
|
||
|
||
void _reload() {
|
||
ref.read(partnersScreenProvider.notifier).reload();
|
||
ref.invalidate(partnerBalancesProvider);
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final t = context.tokens;
|
||
final async = ref.watch(partnersScreenProvider);
|
||
final balances = ref.watch(partnerBalancesProvider).valueOrNull ?? const {};
|
||
|
||
return async.when(
|
||
loading: () => const Center(child: CircularProgressIndicator()),
|
||
error: (e, _) => Center(
|
||
child: Column(
|
||
mainAxisAlignment: MainAxisAlignment.center,
|
||
children: [
|
||
Icon(LucideIcons.cloudOff, size: 40, color: t.muted),
|
||
const SizedBox(height: 12),
|
||
Text('暂无数据,网络不可用', style: TextStyle(color: t.muted)),
|
||
const SizedBox(height: 12),
|
||
DsButton('重试', variant: DsBtnVariant.primary, onPressed: _reload),
|
||
],
|
||
),
|
||
),
|
||
data: (result) {
|
||
final mobile = context.isMobile;
|
||
// 「两者」为窄屏专属 chip:服务端 FIND_IN_SET 无独立值,客户端过滤
|
||
final partners = _typeChip == '两者'
|
||
? result.data.where((p) => p.typeLabel == '两者').toList()
|
||
: result.data;
|
||
// 原型 .main{padding:22px 26px};窄屏对齐 m-partners .m-scroll{padding:14px}
|
||
return Container(
|
||
color: t.bg,
|
||
padding: mobile
|
||
? const EdgeInsets.fromLTRB(AppDims.sp3, AppDims.sp3, AppDims.sp3, 0)
|
||
: const EdgeInsets.fromLTRB(26, 22, 26, 22),
|
||
child: Column(
|
||
children: [
|
||
// ── 头部(原型 .head{margin-bottom:18px});窄屏隐藏——
|
||
// 标题由壳顶栏 .m-top data-title 提供(对齐 m-partners)──
|
||
if (!mobile)
|
||
Container(
|
||
width: double.infinity,
|
||
color: t.bg,
|
||
padding: const EdgeInsets.only(bottom: 18),
|
||
child: Row(
|
||
crossAxisAlignment: CrossAxisAlignment.end,
|
||
children: [
|
||
Text('往来单位',
|
||
style: TextStyle(
|
||
fontSize: AppDims.fsH1,
|
||
fontWeight: FontWeight.w700,
|
||
color: t.heading)),
|
||
const SizedBox(width: AppDims.sp3),
|
||
Padding(
|
||
padding: const EdgeInsets.only(bottom: 2),
|
||
child: Text('供应商与客户档案 · 共 ${result.total} 个',
|
||
style: TextStyle(
|
||
fontSize: AppDims.fsSm, color: t.muted)),
|
||
),
|
||
const Spacer(),
|
||
DsButton('刷新',
|
||
icon: LucideIcons.refreshCw, onPressed: _reload),
|
||
const SizedBox(width: 10),
|
||
WriteGuard(
|
||
child: DsButton('新增往来单位',
|
||
icon: LucideIcons.plus,
|
||
variant: DsBtnVariant.primary,
|
||
onPressed: () => _openForm()),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
Expanded(
|
||
child: DsTable(
|
||
pagerInfoText: '共 ${result.total} 个往来单位',
|
||
emptyText: '没有匹配的往来单位 · 试试调整筛选或搜索',
|
||
toolbar: _toolbar(mobile, partners),
|
||
mobileCards: partners.map((p) => _card(p, balances)).toList(),
|
||
columns: const [
|
||
DsColumn('name', '名称'),
|
||
DsColumn('type', '类型'),
|
||
DsColumn('contact', '联系人'),
|
||
DsColumn('phone', '电话'),
|
||
DsColumn('recv', '应收', numeric: true),
|
||
DsColumn('pay', '应付', numeric: true),
|
||
DsColumn('actions', '操作', action: true),
|
||
],
|
||
rows: partners
|
||
.map((p) => DsRow(
|
||
onTap: () => _openDetail(p, balances),
|
||
cells: _cells(p, balances),
|
||
))
|
||
.toList(),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
},
|
||
);
|
||
}
|
||
|
||
// ── toolbar(原型:searchbox 260 + chips + sp + 重置;
|
||
// 窄屏对齐 m-partners:.m-search 搜索行 + .m-pill 类型横排)──
|
||
Widget _toolbar(bool mobile, List<Partner> partners) {
|
||
void onSearch(String v) {
|
||
_debounce?.cancel();
|
||
_debounce = Timer(AppConstants.searchDebounce, () {
|
||
ref.read(partnersScreenProvider.notifier).setKeyword(v.trim());
|
||
});
|
||
}
|
||
|
||
if (mobile) {
|
||
return Column(
|
||
mainAxisSize: MainAxisSize.min,
|
||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||
children: [
|
||
MSearchRow(
|
||
controller: _searchCtrl,
|
||
hint: '名称 / 拼音',
|
||
onChanged: onSearch,
|
||
),
|
||
const SizedBox(height: AppDims.sp2),
|
||
Row(children: [
|
||
// 类型 pills(原型 m-pill 的既有对应物 DsChip,selected 态)
|
||
Expanded(
|
||
child: SingleChildScrollView(
|
||
scrollDirection: Axis.horizontal,
|
||
child: Row(children: [
|
||
for (final c in _typeChipsMobile) ...[
|
||
DsChip(
|
||
label: c,
|
||
selected: _typeChip == c,
|
||
caret: false,
|
||
onTap: () => _setType(c)),
|
||
const SizedBox(width: 8),
|
||
],
|
||
]),
|
||
),
|
||
),
|
||
WriteGuard(
|
||
child: DsButton('新增',
|
||
small: true,
|
||
icon: LucideIcons.plus,
|
||
variant: DsBtnVariant.primary,
|
||
onPressed: () => _openForm()),
|
||
),
|
||
]),
|
||
],
|
||
);
|
||
}
|
||
|
||
final search = DsSearchBox(
|
||
controller: _searchCtrl,
|
||
hint: '名称 / 拼音',
|
||
width: 260,
|
||
onChanged: onSearch,
|
||
);
|
||
final chips = [
|
||
for (final c in _typeChips) ...[
|
||
DsChip(
|
||
label: c,
|
||
selected: _typeChip == c,
|
||
caret: false,
|
||
onTap: () => _setType(c)),
|
||
const SizedBox(width: 10),
|
||
],
|
||
];
|
||
final reset = DsButton('重置', small: true, onPressed: _clearFilters);
|
||
final export =
|
||
DsButton('导出', small: true, icon: LucideIcons.download, onPressed: () {
|
||
exportExcel(
|
||
filename: '往来单位',
|
||
headers: ['编码', '名称', '类型', '联系人', '电话', '地址', '备注'],
|
||
rows: partners
|
||
.map((p) => [
|
||
p.code ?? '',
|
||
p.name,
|
||
p.typeLabel,
|
||
p.contact ?? '',
|
||
p.phone ?? '',
|
||
p.address ?? '',
|
||
p.remark ?? '',
|
||
])
|
||
.toList(),
|
||
);
|
||
});
|
||
return Row(children: [
|
||
search,
|
||
const SizedBox(width: 10),
|
||
...chips,
|
||
const Spacer(),
|
||
export,
|
||
const SizedBox(width: 10),
|
||
reset
|
||
]);
|
||
}
|
||
|
||
// ── 表格单元格(原型 7 列)──
|
||
List<Widget> _cells(Partner p, Map<int, ({double recv, double pay})> bal) {
|
||
final t = context.tokens;
|
||
final b = bal[p.id];
|
||
return [
|
||
// 名称双行:.pname + .pcode(地址前 12 字…)
|
||
Column(
|
||
mainAxisSize: MainAxisSize.min,
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(p.name,
|
||
style: TextStyle(fontWeight: FontWeight.w600, color: t.heading)),
|
||
// 地址为空则不渲染副行(不占位、不显示「—」),行高回落单行
|
||
if (p.address?.isNotEmpty == true)
|
||
Padding(
|
||
padding: const EdgeInsets.only(top: 2),
|
||
child: Text(_addrBrief(p.address!),
|
||
style: TextStyle(
|
||
fontSize: AppDims.fsXs,
|
||
color: t.faint,
|
||
fontFamily: AppFonts.mono,
|
||
fontFamilyFallback: AppFonts.monoFallback)),
|
||
),
|
||
],
|
||
),
|
||
partnerTypeBadge(p),
|
||
Text((p.contact?.isNotEmpty == true) ? p.contact! : '—'),
|
||
Text((p.phone?.isNotEmpty == true) ? p.phone! : '—',
|
||
style: const TextStyle(
|
||
fontFamily: AppFonts.mono,
|
||
fontFamilyFallback: AppFonts.monoFallback)),
|
||
Text(fmtYuan(b?.recv ?? 0),
|
||
style: const TextStyle(
|
||
fontFamily: AppFonts.mono,
|
||
fontFamilyFallback: AppFonts.monoFallback)),
|
||
Text(fmtYuan(b?.pay ?? 0),
|
||
style: const TextStyle(
|
||
fontFamily: AppFonts.mono,
|
||
fontFamilyFallback: AppFonts.monoFallback)),
|
||
Row(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
_iconBtn(LucideIcons.eye, '详情', () => _openDetail(p, bal)),
|
||
const SizedBox(width: 8),
|
||
WriteGuard(
|
||
child: _iconBtn(LucideIcons.pencil, '编辑', () => _openForm(p)),
|
||
),
|
||
],
|
||
),
|
||
];
|
||
}
|
||
|
||
/// 原型 .pcode = addr.slice(0,12)…(地址前 12 字 + 省略号)
|
||
String _addrBrief(String addr) =>
|
||
addr.length > 12 ? '${addr.substring(0, 12)}…' : addr;
|
||
|
||
Widget _iconBtn(IconData icon, String tip, VoidCallback onTap) {
|
||
final t = context.tokens;
|
||
return Tooltip(
|
||
message: tip,
|
||
child: InkWell(
|
||
onTap: onTap,
|
||
borderRadius: BorderRadius.circular(AppDims.rSm),
|
||
child: Padding(
|
||
padding: const EdgeInsets.all(2),
|
||
child: Icon(icon, size: 16, color: t.muted),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
/// 类型徽章图标变体(原型 m-partners:badge b-供应商/客户/两者 + BADGE_ICON)。
|
||
/// 色调与 partnerTypeBadge(partner_detail_drawer)一致,仅多前导图标。
|
||
DsBadge _typeBadgeIcon(Partner p) => DsBadge(p.typeLabel,
|
||
tone: switch (p.typeLabel) {
|
||
'两者' => DsBadgeTone.warn,
|
||
'客户' => DsBadgeTone.accent,
|
||
_ => DsBadgeTone.info,
|
||
},
|
||
icon: statusIcon(p.typeLabel));
|
||
|
||
/// 状态徽章图标变体(原型 badge b-启用/b-停用)。
|
||
DsBadge _statusBadgeIcon(Partner p) {
|
||
final label = p.status == 'disabled' ? '停用' : '启用';
|
||
return DsBadge(label,
|
||
tone: label == '启用' ? DsBadgeTone.ok : DsBadgeTone.muted,
|
||
icon: statusIcon(label));
|
||
}
|
||
|
||
// ── 窄屏卡片(m-partners:mc-sub mono 编码 + 右侧类型/状态图标徽章)──
|
||
Widget _card(Partner p, Map<int, ({double recv, double pay})> bal) {
|
||
final b = bal[p.id];
|
||
return MobileListCard(
|
||
title: Text(p.name),
|
||
subtitle: (p.code?.isNotEmpty == true)
|
||
? Text(p.code!,
|
||
style: const TextStyle(
|
||
fontFamily: AppFonts.mono,
|
||
fontFamilyFallback: AppFonts.monoFallback))
|
||
: null,
|
||
trailing: Row(mainAxisSize: MainAxisSize.min, children: [
|
||
_typeBadgeIcon(p),
|
||
const SizedBox(width: 6),
|
||
_statusBadgeIcon(p),
|
||
]),
|
||
fields: [
|
||
if (p.contact?.isNotEmpty == true) MobileCardField('联系人', p.contact),
|
||
if (p.phone?.isNotEmpty == true) MobileCardField('电话', p.phone),
|
||
MobileCardField('应收', fmtYuan(b?.recv ?? 0)),
|
||
MobileCardField('应付', fmtYuan(b?.pay ?? 0)),
|
||
],
|
||
onTap: () => _openDetail(p, bal),
|
||
);
|
||
}
|
||
|
||
void _openDetail(Partner p, Map<int, ({double recv, double pay})> bal) {
|
||
showPartnerDetailDrawer(
|
||
context,
|
||
partner: p,
|
||
balance: bal[p.id],
|
||
onEdit: WriteGuard.isReadonly(ref) ? null : () => _openForm(p),
|
||
);
|
||
}
|
||
|
||
void _openForm([Partner? partner]) {
|
||
showAppDialog(
|
||
context: context,
|
||
builder: (ctx) => _PartnerFormDialog(
|
||
partner: partner,
|
||
onSaved: _reload,
|
||
onDelete: partner == null ? null : () => _confirmDelete(partner),
|
||
),
|
||
);
|
||
}
|
||
|
||
Future<void> _confirmDelete(Partner partner) async {
|
||
final confirmed = await showDialog<bool>(
|
||
context: context,
|
||
builder: (ctx) => AlertDialog(
|
||
title: const Text('确认删除'),
|
||
content: Text('确认删除「${partner.name}」?此操作不可恢复。'),
|
||
actions: [
|
||
DsButton('取消', onPressed: () => Navigator.of(ctx).pop(false)),
|
||
DsButton('删除',
|
||
variant: DsBtnVariant.danger,
|
||
onPressed: () => Navigator.of(ctx).pop(true)),
|
||
],
|
||
),
|
||
);
|
||
if (confirmed == true && mounted) {
|
||
try {
|
||
await ref
|
||
.read(partnersScreenProvider.notifier)
|
||
.deletePartner(partner.id);
|
||
if (mounted) {
|
||
showDsToast(context, '删除成功', bg: context.tokens.success);
|
||
}
|
||
} catch (e) {
|
||
if (mounted) {
|
||
showDsToast(context, '删除失败:$e', bg: context.tokens.danger);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// ── 新增/编辑弹窗(原型 openAdd 的 frow 布局:名称*+类型 / 联系人+电话 / 地址 /
|
||
// 期初余额 + 备注(备注为既有能力保留))──
|
||
class _PartnerFormDialog extends ConsumerStatefulWidget {
|
||
final Partner? partner;
|
||
final VoidCallback onSaved;
|
||
final VoidCallback? onDelete;
|
||
|
||
const _PartnerFormDialog(
|
||
{this.partner, required this.onSaved, this.onDelete});
|
||
|
||
@override
|
||
ConsumerState<_PartnerFormDialog> createState() => _PartnerFormDialogState();
|
||
}
|
||
|
||
class _PartnerFormDialogState extends ConsumerState<_PartnerFormDialog> {
|
||
final _formKey = GlobalKey<FormState>();
|
||
late final TextEditingController _nameCtrl;
|
||
late final TextEditingController _contactCtrl;
|
||
late final TextEditingController _phoneCtrl;
|
||
late final TextEditingController _addressCtrl;
|
||
late final TextEditingController _balanceCtrl;
|
||
late final TextEditingController _remarkCtrl;
|
||
late String _type; // supplier / customer / supplier,customer
|
||
bool _saving = false;
|
||
|
||
static const _typeOptions = {
|
||
'supplier': '供应商',
|
||
'customer': '客户',
|
||
'supplier,customer': '两者',
|
||
};
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
final p = widget.partner;
|
||
_nameCtrl = TextEditingController(text: p?.name ?? '');
|
||
_contactCtrl = TextEditingController(text: p?.contact ?? '');
|
||
_phoneCtrl = TextEditingController(text: p?.phone ?? '');
|
||
_addressCtrl = TextEditingController(text: p?.address ?? '');
|
||
_balanceCtrl = TextEditingController(
|
||
text: (p?.balance ?? 0) == 0 ? '' : '${p!.balance}');
|
||
_remarkCtrl = TextEditingController(text: p?.remark ?? '');
|
||
// SET 值可能乱序(customer,supplier);归一化到选项 key
|
||
final raw = p?.type ?? 'supplier';
|
||
final isS = raw.contains('supplier'), isC = raw.contains('customer');
|
||
_type = isS && isC ? 'supplier,customer' : (isC ? 'customer' : 'supplier');
|
||
}
|
||
|
||
@override
|
||
void dispose() {
|
||
_nameCtrl.dispose();
|
||
_contactCtrl.dispose();
|
||
_phoneCtrl.dispose();
|
||
_addressCtrl.dispose();
|
||
_balanceCtrl.dispose();
|
||
_remarkCtrl.dispose();
|
||
super.dispose();
|
||
}
|
||
|
||
Future<void> _save() async {
|
||
if (!_formKey.currentState!.validate()) return;
|
||
setState(() => _saving = true);
|
||
final data = {
|
||
'name': _nameCtrl.text.trim(),
|
||
'type': _type,
|
||
'contact': _contactCtrl.text.trim(),
|
||
'phone': _phoneCtrl.text.trim(),
|
||
'address': _addressCtrl.text.trim(),
|
||
'balance': double.tryParse(_balanceCtrl.text.trim()) ?? 0,
|
||
'remark': _remarkCtrl.text.trim(),
|
||
if (widget.partner?.code != null) 'code': widget.partner!.code,
|
||
};
|
||
try {
|
||
final notifier = ref.read(partnersScreenProvider.notifier);
|
||
if (widget.partner != null) {
|
||
await notifier.updatePartner(widget.partner!.id, data);
|
||
} else {
|
||
await notifier.createPartner(data);
|
||
}
|
||
if (mounted) {
|
||
Navigator.of(context).pop();
|
||
widget.onSaved();
|
||
showDsToast(context, widget.partner != null ? '更新成功' : '创建成功',
|
||
bg: context.tokens.success);
|
||
}
|
||
} catch (e) {
|
||
if (mounted) {
|
||
showDsToast(context, '保存失败:$e', bg: context.tokens.danger);
|
||
}
|
||
} finally {
|
||
if (mounted) setState(() => _saving = false);
|
||
}
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final isEdit = widget.partner != null;
|
||
return Dialog(
|
||
child: Container(
|
||
width: context.dialogWidth(560),
|
||
padding: const EdgeInsets.all(24),
|
||
child: Form(
|
||
key: _formKey,
|
||
child: SingleChildScrollView(
|
||
child: Column(
|
||
mainAxisSize: MainAxisSize.min,
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(isEdit ? '编辑往来单位' : '新增往来单位',
|
||
style: const TextStyle(
|
||
fontSize: 18, fontWeight: FontWeight.w600)),
|
||
const SizedBox(height: 20),
|
||
// frow1:名称* + 类型*
|
||
Row(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||
Expanded(
|
||
child: DsField('名称',
|
||
required: true,
|
||
input: TextFormField(
|
||
controller: _nameCtrl,
|
||
decoration:
|
||
const InputDecoration(hintText: '如:茅台华东总代'),
|
||
validator: (v) =>
|
||
(v == null || v.trim().isEmpty) ? '不能为空' : null,
|
||
)),
|
||
),
|
||
const SizedBox(width: 14),
|
||
Expanded(
|
||
child: DsField('类型',
|
||
required: true,
|
||
input: DsSelect<String>(
|
||
value: _type,
|
||
options: [
|
||
for (final e in _typeOptions.entries)
|
||
(e.key, e.value),
|
||
],
|
||
onChanged: (v) => setState(() => _type = v),
|
||
)),
|
||
),
|
||
]),
|
||
const SizedBox(height: 14),
|
||
// frow2:联系人 + 电话
|
||
Row(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||
Expanded(
|
||
child: DsField('联系人',
|
||
input: TextFormField(
|
||
controller: _contactCtrl,
|
||
decoration: const InputDecoration(hintText: '姓名'),
|
||
)),
|
||
),
|
||
const SizedBox(width: 14),
|
||
Expanded(
|
||
child: DsField('电话',
|
||
input: TextFormField(
|
||
controller: _phoneCtrl,
|
||
decoration:
|
||
const InputDecoration(hintText: '手机 / 座机'),
|
||
)),
|
||
),
|
||
]),
|
||
const SizedBox(height: 14),
|
||
// frow3:地址(单列占满)
|
||
DsField('地址',
|
||
input: TextFormField(
|
||
controller: _addressCtrl,
|
||
decoration:
|
||
const InputDecoration(hintText: '省 / 市 / 详细地址'),
|
||
)),
|
||
const SizedBox(height: 14),
|
||
// frow4:期初余额 + 备注
|
||
Row(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||
Expanded(
|
||
child: DsField('期初余额',
|
||
input: TextFormField(
|
||
controller: _balanceCtrl,
|
||
keyboardType: const TextInputType.numberWithOptions(
|
||
decimal: true),
|
||
decoration: const InputDecoration(hintText: '0.00'),
|
||
)),
|
||
),
|
||
const SizedBox(width: 14),
|
||
Expanded(
|
||
child: DsField('备注',
|
||
input: TextFormField(
|
||
controller: _remarkCtrl,
|
||
decoration: const InputDecoration(hintText: '备注说明'),
|
||
)),
|
||
),
|
||
]),
|
||
const SizedBox(height: 24),
|
||
Row(
|
||
children: [
|
||
if (widget.onDelete != null)
|
||
WriteGuard(
|
||
child: DsButton('删除', variant: DsBtnVariant.danger,
|
||
onPressed: () {
|
||
Navigator.of(context).pop();
|
||
widget.onDelete!();
|
||
}),
|
||
),
|
||
const Spacer(),
|
||
DsButton('取消',
|
||
onPressed: () => Navigator.of(context).pop()),
|
||
const SizedBox(width: 10),
|
||
DsButton(_saving ? '保存中…' : '保存',
|
||
variant: DsBtnVariant.primary,
|
||
onPressed: _saving ? null : _save),
|
||
],
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|