import '../../core/utils/dialog_util.dart'; import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../core/models/page_result.dart'; import '../../core/responsive/responsive.dart'; import '../../core/config/app_constants.dart'; import '../../core/theme/context_tokens.dart'; import '../../core/theme/app_dims.g.dart'; import '../../models/partner.dart'; import '../../providers/partner_provider.dart'; import '../../widgets/data_table_card.dart'; import '../../widgets/kpi_card.dart'; import '../../widgets/mobile_list_card.dart'; import '../../widgets/page_scaffold.dart'; import '../../core/utils/export_util.dart'; import '../../widgets/write_guard.dart'; class PartnersScreen extends ConsumerStatefulWidget { const PartnersScreen({super.key}); @override ConsumerState createState() => _PartnersScreenState(); } class _PartnersScreenState extends ConsumerState { final _supplierSearchCtrl = TextEditingController(); final _customerSearchCtrl = TextEditingController(); Timer? _supplierDebounce; Timer? _customerDebounce; @override void dispose() { _supplierSearchCtrl.dispose(); _customerSearchCtrl.dispose(); _supplierDebounce?.cancel(); _customerDebounce?.cancel(); super.dispose(); } @override Widget build(BuildContext context) { return PageScaffold( title: '往来单位', tabs: const [ Tab(text: '供应商'), Tab(text: '客户'), ], tabViews: [ _buildSupplierTab(), _buildCustomerTab(), ], ); } Widget _buildSupplierTab() { final asyncPartners = ref.watch(supplierListProvider); return asyncPartners.when( loading: () => const Center(child: CircularProgressIndicator()), error: (e, _) => Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.cloud_off, size: 40, color: context.tokens.muted), const SizedBox(height: 12), Text('暂无数据,网络不可用', style: TextStyle(color: context.tokens.muted)), const SizedBox(height: 12), ElevatedButton( onPressed: () => ref.read(supplierListProvider.notifier).reload(), child: const Text('重试'), ), ], ), ), data: (result) => _buildPartnerList( result, isSupplier: true, searchCtrl: _supplierSearchCtrl, onSearchChanged: (v) { _supplierDebounce?.cancel(); _supplierDebounce = Timer(AppConstants.searchDebounce, () { ref.read(supplierListProvider.notifier).setKeyword(v); }); }, onPageChanged: (p) => ref.read(supplierListProvider.notifier).setPage(p), onPageSizeChanged: (s) => ref.read(supplierListProvider.notifier).setPageSize(s), onAdd: () => _showPartnerDialog(context, isSupplier: true), onEdit: (p) => _showPartnerDialog(context, isSupplier: true, partner: p), onDelete: (p) => _confirmDelete(context, p, isSupplier: true), ), ); } Widget _buildCustomerTab() { final asyncPartners = ref.watch(customerListProvider); return asyncPartners.when( loading: () => const Center(child: CircularProgressIndicator()), error: (e, _) => Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.cloud_off, size: 40, color: context.tokens.muted), const SizedBox(height: 12), Text('暂无数据,网络不可用', style: TextStyle(color: context.tokens.muted)), const SizedBox(height: 12), ElevatedButton( onPressed: () => ref.read(customerListProvider.notifier).reload(), child: const Text('重试'), ), ], ), ), data: (result) => _buildPartnerList( result, isSupplier: false, searchCtrl: _customerSearchCtrl, onSearchChanged: (v) { _customerDebounce?.cancel(); _customerDebounce = Timer(AppConstants.searchDebounce, () { ref.read(customerListProvider.notifier).setKeyword(v); }); }, onPageChanged: (p) => ref.read(customerListProvider.notifier).setPage(p), onPageSizeChanged: (s) => ref.read(customerListProvider.notifier).setPageSize(s), onAdd: () => _showPartnerDialog(context, isSupplier: false), onEdit: (p) => _showPartnerDialog(context, isSupplier: false, partner: p), onDelete: (p) => _confirmDelete(context, p, isSupplier: false), ), ); } Widget _buildPartnerList( PageResult result, { required bool isSupplier, required TextEditingController searchCtrl, required void Function(String) onSearchChanged, required void Function(int) onPageChanged, required void Function(int) onPageSizeChanged, required VoidCallback onAdd, required void Function(Partner) onEdit, required void Function(Partner) onDelete, }) { final partners = result.data; Widget partnerCard(Partner p) { return MobileListCard( title: Text(p.name), subtitle: p.code != null ? Text(p.code!) : null, trailing: _PartnerStatusPill(p.status), fields: [ if (p.contact?.isNotEmpty == true) MobileCardField('联系人', p.contact), if (p.phone?.isNotEmpty == true) MobileCardField('电话', p.phone), if (p.address?.isNotEmpty == true) MobileCardField('地址', p.address), ], actions: WriteGuard.isReadonly(ref) ? const [] : [ WriteGuard( child: TextButton( key: Key('btn_edit_${p.id}'), onPressed: () => onEdit(p), child: const Text('编辑', style: TextStyle(fontSize: 13)), ), ), WriteGuard( child: TextButton( key: Key('btn_delete_${p.id}'), onPressed: () => onDelete(p), child: Text('删除', style: TextStyle(fontSize: 13, color: context.tokens.danger)), ), ), ], ); } return Column( children: [ // 副标(还原原型 .head .sub) Container( width: double.infinity, color: context.tokens.bg, padding: const EdgeInsets.fromLTRB( AppDims.sp3, AppDims.sp3, AppDims.sp3, 0), child: Text( '共 ${result.total} 个${isSupplier ? '供应商' : '客户'}', style: TextStyle( fontSize: AppDims.fsSm, color: context.tokens.muted), ), ), Expanded( child: DataTableCard( totalCount: result.total, page: result.page, pageSize: result.pageSize, onPageChanged: onPageChanged, onPageSizeChanged: onPageSizeChanged, mobileCards: partners.map(partnerCard).toList(), toolbar: Builder(builder: (ctx) { final searchField = TextField( controller: searchCtrl, decoration: InputDecoration( hintText: isSupplier ? '搜索供应商名称/编码' : '搜索客户名称/编码', prefixIcon: const Icon(Icons.search, size: 16), hintStyle: const TextStyle(fontSize: 13), ), onChanged: onSearchChanged, ); final buttons = [ if (!WriteGuard.isReadonly(ref)) ...[ WriteGuard( child: ElevatedButton.icon( onPressed: onAdd, icon: const Icon(Icons.add, size: 16), label: const Text('新建'), ), ), const SizedBox(width: AppDims.sp2), ], OutlinedButton.icon( onPressed: () => exportExcel( filename: isSupplier ? '供应商列表' : '客户列表', headers: ['编码', '名称', '联系人', '电话', '地址', '备注'], rows: partners.map((p) => [ p.code ?? '', p.name, p.contact ?? '', p.phone ?? '', p.address ?? '', p.remark ?? '', ]).toList(), ), icon: const Icon(Icons.download, size: 16), label: const Text('导出'), ), ]; if (ctx.isMobile) { // 窄屏:搜索独占一行 + 操作按钮一行 return Column( mainAxisSize: MainAxisSize.min, children: [ searchField, const SizedBox(height: AppDims.sp2), Row(children: [...buttons, const Spacer()]), ], ); } return Row( children: [ ...buttons, const Spacer(), SizedBox(width: 240, child: searchField), ], ); }), columns: const [ DataColumn(label: Text('名称')), DataColumn(label: Text('联系人')), DataColumn(label: Text('联系电话')), DataColumn(label: Text('地址')), DataColumn(label: Text('状态')), DataColumn(label: Text('操作')), ], rows: partners.isEmpty ? [ DataRow(cells: [ DataCell(Text(isSupplier ? '暂无供应商' : '暂无客户', style: TextStyle(color: context.tokens.muted))), const DataCell(SizedBox()), const DataCell(SizedBox()), const DataCell(SizedBox()), const DataCell(SizedBox()), const DataCell(SizedBox()), ]) ] : partners .map((p) => DataRow( cells: [ DataCell(SizedBox( width: 200, child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(p.name, overflow: TextOverflow.ellipsis, style: const TextStyle( fontWeight: FontWeight.w600)), if ((p.code ?? '').isNotEmpty) Text(p.code!, style: TextStyle( fontFamily: 'monospace', fontSize: AppDims.fsXs, color: context.tokens.muted)), ], ), )), DataCell(Text(p.contact ?? '-')), DataCell(Text(p.phone ?? '-')), DataCell(SizedBox( width: 160, child: Text(p.address ?? '-', overflow: TextOverflow.ellipsis), )), DataCell(_PartnerStatusPill(p.status)), DataCell(Row( mainAxisSize: MainAxisSize.min, children: WriteGuard.isReadonly(ref) ? const [] : [ WriteGuard( child: TextButton( key: Key('btn_edit_${p.id}'), onPressed: () => onEdit(p), child: const Text('编辑', style: TextStyle(fontSize: 12)), ), ), WriteGuard( child: TextButton( key: Key('btn_delete_${p.id}'), onPressed: () => onDelete(p), child: Text('删除', style: TextStyle( fontSize: 12, color: context.tokens.danger)), ), ), ], )), ], )) .toList(), ), ), ], ); } void _showPartnerDialog(BuildContext context, {required bool isSupplier, Partner? partner}) { showAppDialog( context: context, builder: (ctx) => _PartnerFormDialog( isSupplier: isSupplier, partner: partner, onSaved: () { if (isSupplier) { ref.read(supplierListProvider.notifier).reload(); } else { ref.read(customerListProvider.notifier).reload(); } }, ), ); } Future _confirmDelete(BuildContext context, Partner partner, {required bool isSupplier}) async { final confirmed = await showDialog( context: context, builder: (ctx) => AlertDialog( title: const Text('确认删除'), content: Text('确认删除「${partner.name}」?此操作不可恢复。'), actions: [ TextButton( onPressed: () => Navigator.of(ctx).pop(false), child: const Text('取消'), ), ElevatedButton( onPressed: () => Navigator.of(ctx).pop(true), style: ElevatedButton.styleFrom( backgroundColor: context.tokens.danger, foregroundColor: Colors.white), child: const Text('删除'), ), ], ), ); if (confirmed == true && mounted) { try { if (isSupplier) { await ref .read(supplierListProvider.notifier) .deletePartner(partner.id); } else { await ref .read(customerListProvider.notifier) .deletePartner(partner.id); } if (mounted) { ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: const Text('删除成功'), backgroundColor: context.tokens.success)); } } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: Text('删除失败:$e'), backgroundColor: context.tokens.danger)); } } } } } // 往来单位状态徽章(启用/停用),还原原型 .badge 圆点软底。 class _PartnerStatusPill extends StatelessWidget { final String status; const _PartnerStatusPill(this.status); @override Widget build(BuildContext context) { final t = context.tokens; return status == 'enabled' ? StatusPill(label: '启用', color: t.success, background: t.okSoft) : StatusPill(label: '停用', color: t.muted, background: t.borderSubtle); } } class _PartnerFormDialog extends ConsumerStatefulWidget { final bool isSupplier; final Partner? partner; final VoidCallback onSaved; const _PartnerFormDialog({ required this.isSupplier, this.partner, required this.onSaved, }); @override ConsumerState<_PartnerFormDialog> createState() => _PartnerFormDialogState(); } class _PartnerFormDialogState extends ConsumerState<_PartnerFormDialog> { final _formKey = GlobalKey(); late final TextEditingController _nameCtrl; late final TextEditingController _codeCtrl; late final TextEditingController _contactCtrl; late final TextEditingController _phoneCtrl; late final TextEditingController _addressCtrl; late final TextEditingController _remarkCtrl; bool _saving = false; @override void initState() { super.initState(); final p = widget.partner; _nameCtrl = TextEditingController(text: p?.name ?? ''); _codeCtrl = TextEditingController(text: p?.code ?? ''); _contactCtrl = TextEditingController(text: p?.contact ?? ''); _phoneCtrl = TextEditingController(text: p?.phone ?? ''); _addressCtrl = TextEditingController(text: p?.address ?? ''); _remarkCtrl = TextEditingController(text: p?.remark ?? ''); } @override void dispose() { _nameCtrl.dispose(); _codeCtrl.dispose(); _contactCtrl.dispose(); _phoneCtrl.dispose(); _addressCtrl.dispose(); _remarkCtrl.dispose(); super.dispose(); } Future _save() async { if (!_formKey.currentState!.validate()) return; setState(() => _saving = true); final data = { 'name': _nameCtrl.text.trim(), 'type': widget.isSupplier ? 'supplier' : 'customer', if (_codeCtrl.text.trim().isNotEmpty) 'code': _codeCtrl.text.trim(), if (_contactCtrl.text.trim().isNotEmpty) 'contact': _contactCtrl.text.trim(), if (_phoneCtrl.text.trim().isNotEmpty) 'phone': _phoneCtrl.text.trim(), if (_addressCtrl.text.trim().isNotEmpty) 'address': _addressCtrl.text.trim(), if (_remarkCtrl.text.trim().isNotEmpty) 'remark': _remarkCtrl.text.trim(), }; try { final notifier = widget.isSupplier ? ref.read(supplierListProvider.notifier) : ref.read(customerListProvider.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(); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(widget.partner != null ? '更新成功' : '创建成功'), backgroundColor: context.tokens.success, ), ); } } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('保存失败:$e'), backgroundColor: context.tokens.danger), ); } } finally { if (mounted) setState(() => _saving = false); } } @override Widget build(BuildContext context) { final isEdit = widget.partner != null; final typeName = widget.isSupplier ? '供应商' : '客户'; return Dialog( child: Container( width: context.dialogWidth(520), padding: const EdgeInsets.all(24), child: Form( key: _formKey, child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(isEdit ? '编辑$typeName' : '新建$typeName', style: const TextStyle( fontSize: 18, fontWeight: FontWeight.w600)), const SizedBox(height: 20), TextFormField( controller: _nameCtrl, decoration: InputDecoration(labelText: '$typeName名称'), validator: (v) => (v == null || v.isEmpty) ? '不能为空' : null, ), const SizedBox(height: 12), Row(children: [ if (isEdit) ...[ Expanded( child: TextFormField( controller: _codeCtrl, decoration: const InputDecoration(labelText: '编码'), ), ), const SizedBox(width: 12), ], Expanded( child: TextFormField( controller: _contactCtrl, decoration: const InputDecoration(labelText: '联系人'), ), ), ]), const SizedBox(height: 12), TextFormField( controller: _phoneCtrl, decoration: const InputDecoration(labelText: '联系电话'), ), const SizedBox(height: 12), TextFormField( controller: _addressCtrl, decoration: const InputDecoration(labelText: '地址'), ), const SizedBox(height: 12), TextFormField( controller: _remarkCtrl, decoration: const InputDecoration(labelText: '备注'), maxLines: 2, ), const SizedBox(height: 24), Row( mainAxisAlignment: MainAxisAlignment.end, children: [ TextButton( onPressed: () => Navigator.of(context).pop(), child: const Text('取消'), ), const SizedBox(width: 8), ElevatedButton( onPressed: _saving ? null : _save, child: _saving ? const SizedBox( width: 16, height: 16, child: CircularProgressIndicator( strokeWidth: 2, color: Colors.white)) : const Text('保存'), ), ], ), ], ), ), ), ); } }