import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import '../../core/theme/app_theme.dart'; import '../../models/inventory.dart'; import '../../providers/inventory_provider.dart'; import '../../widgets/data_table_card.dart'; import '../../widgets/multi_select_dropdown.dart' show ColDef, ColumnToggleButton, FilterableColumnHeader; import '../../providers/connectivity_provider.dart'; import '../../core/utils/export_util.dart'; class BatchTrackingScreen extends ConsumerStatefulWidget { const BatchTrackingScreen({super.key}); @override ConsumerState createState() => _BatchTrackingScreenState(); } class _BatchTrackingScreenState extends ConsumerState { int _page = 1; int _pageSize = 20; int _total = 0; late Future> _future; List _records = []; Set _filterStatus = {}; Set _filterWarehouse = {}; Set _filterSupplier = {}; Set _hiddenCols = {}; static const _colDefs = [ ColDef('product', '商品', required: true), ColDef('spec', '规格', minWidth: 1100), ColDef('batch', '批次号', minWidth: 1000), ColDef('order_no', '入库单号', minWidth: 900), ColDef('supplier', '供应商', minWidth: 1100), ColDef('warehouse', '仓库'), ColDef('date', '入库日期', minWidth: 1000), ColDef('qty', '数量'), ColDef('price', '单价', minWidth: 900), ColDef('status', '状态'), ColDef('buyer', '买家/时间'), ]; @override void initState() { super.initState(); _fetch(); } void _fetch() { _future = ref .read(inventoryRepositoryProvider) .listProducts(page: _page, pageSize: _pageSize) .then((r) { _total = r.total; _records = r.data; return r.data; }); } void _refetch() => setState(() => _fetch()); List _applyFilters( List all) { return all.where((r) { if (_filterStatus.isNotEmpty) { final label = r.isSoldOut ? '已卖出' : '在售'; if (!_filterStatus.contains(label)) return false; } if (_filterWarehouse.isNotEmpty) { if (!_filterWarehouse.contains(r.warehouseName ?? '')) return false; } if (_filterSupplier.isNotEmpty) { if (!_filterSupplier.contains(r.supplierName ?? '')) return false; } return true; }).toList(); } @override Widget build(BuildContext context) { // 网络恢复时自动刷新 ref.listen(networkRecoveryCountProvider, (_, __) => _refetch()); return FutureBuilder>( future: _future, builder: (context, snap) { if (snap.connectionState == ConnectionState.waiting) { return const Center(child: CircularProgressIndicator()); } if (snap.hasError) { // 有缓存数据时展示缓存,顶部加提示条 if (_records.isNotEmpty) { return Column( children: [ _OfflineBanner(onRetry: _refetch), Expanded(child: _buildTable(_applyFilters(_records))), ], ); } return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon(Icons.cloud_off, size: 40, color: AppTheme.textSecondary), const SizedBox(height: 12), const Text('暂无数据,网络不可用', style: TextStyle(color: AppTheme.textSecondary)), const SizedBox(height: 12), ElevatedButton(onPressed: _refetch, child: const Text('重试')), ], ), ); } final filtered = _applyFilters(_records); return _buildTable(filtered); }, ); } Widget _buildTable(List records) { // Derive filter options from all loaded records final warehouseOptions = _records .map((r) => r.warehouseName ?? '') .where((s) => s.isNotEmpty) .toSet() .toList() ..sort(); final supplierOptions = _records .map((r) => r.supplierName ?? '') .where((s) => s.isNotEmpty) .toSet() .toList() ..sort(); // Build visible columns (respect manual hide + responsive auto-hide) final screenWidth = MediaQuery.of(context).size.width; final visibleCols = _colDefs .where((c) => !_hiddenCols.contains(c.key) && (c.minWidth == null || screenWidth >= c.minWidth!)) .toList(); final columns = visibleCols.map((c) { final label = switch (c.key) { 'status' => FilterableColumnHeader( text: c.label, options: const ['在售', '已卖出'], selected: _filterStatus, onChanged: (v) => setState(() => _filterStatus = v), ), 'warehouse' => FilterableColumnHeader( text: c.label, options: warehouseOptions, selected: _filterWarehouse, onChanged: (v) => setState(() => _filterWarehouse = v), ), 'supplier' => FilterableColumnHeader( text: c.label, options: supplierOptions, selected: _filterSupplier, onChanged: (v) => setState(() => _filterSupplier = v), ), _ => Text(c.label), }; return DataColumn( label: label, numeric: c.key == 'qty' || c.key == 'price'); }).toList(); final rows = records.isEmpty ? [ DataRow( cells: List.generate( visibleCols.length, (i) => i == 1 ? const DataCell(Text('暂无记录', style: TextStyle(color: AppTheme.textSecondary))) : const DataCell(SizedBox()), ), ), ] : records .map((r) => DataRow( cells: visibleCols .map((c) => _buildCell(c.key, r)) .toList(), )) .toList(); return DataTableCard( totalCount: _total, page: _page, pageSize: _pageSize, onPageChanged: (p) => setState(() { _page = p; _fetch(); }), onPageSizeChanged: (s) => setState(() { _pageSize = s; _page = 1; _fetch(); }), toolbar: Row( children: [ const Text('已审核入库商品(含库存与销售状态)', style: TextStyle(fontSize: 13, color: AppTheme.textSecondary)), const Spacer(), OutlinedButton.icon( onPressed: () => exportExcel( filename: '商品管理', headers: ['商品名称', '商品编码', '规格', '批次号', '入库单号', '供应商', '仓库', '入库日期', '数量', '单价', '状态', '买家'], rows: records.map((r) => [ r.productName ?? '', r.productCode ?? '', r.productSpec ?? '', r.batchNo ?? '', r.orderNo ?? '', r.supplierName ?? '', r.warehouseName ?? '', r.orderDate?.substring(0, 10) ?? '', r.quantity.toInt(), r.unitPrice, r.isSoldOut ? '已卖出' : '在售', r.buyerName ?? '', ]).toList(), ), icon: const Icon(Icons.download, size: 16), label: const Text('导出'), ), const SizedBox(width: 4), IconButton( icon: const Icon(Icons.refresh, size: 18), onPressed: () => setState(() { _page = 1; _fetch(); }), tooltip: '刷新', ), const SizedBox(width: 4), ColumnToggleButton( columns: _colDefs, hidden: _hiddenCols, onChanged: (v) => setState(() => _hiddenCols = v), ), ], ), columns: columns, rows: rows, ); } DataCell _buildCell(String key, ProductTrackingRecord r) { final batchText = (r.batchNo != null && r.batchNo!.isNotEmpty) ? r.batchNo! : null; switch (key) { case 'product': return DataCell( GestureDetector( onTap: () => context.push('/products/${r.productId}'), child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.center, children: [ Text(r.productName ?? '-', style: const TextStyle( fontSize: 13, fontWeight: FontWeight.w500, color: AppTheme.primary, decoration: TextDecoration.underline, decorationColor: AppTheme.primary)), if (r.productCode != null) Text(r.productCode!, style: const TextStyle( fontSize: 11, color: AppTheme.textSecondary, fontFamily: 'monospace')), ], ), ), ); case 'spec': return DataCell(Text(r.productSpec ?? '-', style: const TextStyle(fontSize: 12))); case 'batch': return DataCell(batchText != null ? Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( color: AppTheme.primary.withOpacity(0.08), borderRadius: BorderRadius.circular(3), ), child: Text(batchText, style: const TextStyle( fontSize: 12, color: AppTheme.primary, fontFamily: 'monospace')), ) : const Text('无批次', style: TextStyle( fontSize: 12, color: AppTheme.textSecondary))); case 'order_no': return DataCell(Text(r.orderNo ?? '-', style: const TextStyle( fontSize: 11, color: AppTheme.primary, fontFamily: 'monospace'))); case 'supplier': return DataCell(Text(r.supplierName ?? '-', style: const TextStyle(fontSize: 12))); case 'warehouse': return DataCell(Text(r.warehouseName ?? '-', style: const TextStyle(fontSize: 12))); case 'date': return DataCell(Text(r.orderDate?.substring(0, 10) ?? '-', style: const TextStyle(fontSize: 12))); case 'qty': return DataCell(Text( '${r.quantity.toStringAsFixed(0)} ${r.productUnit ?? ''}', style: const TextStyle(fontWeight: FontWeight.w500))); case 'price': return DataCell(Text('¥${r.unitPrice.toStringAsFixed(2)}', style: const TextStyle(fontSize: 13))); case 'status': return DataCell(_StatusBadge(r.isSoldOut)); case 'buyer': return DataCell(r.isSoldOut ? Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.center, children: [ Text( r.buyerName?.isNotEmpty == true ? r.buyerName! : '未知买家', style: const TextStyle( fontSize: 12, fontWeight: FontWeight.w500), ), if (r.soldAt != null) Text( r.soldAt!.length > 10 ? r.soldAt!.substring(0, 10) : r.soldAt!, style: const TextStyle( fontSize: 11, color: AppTheme.textSecondary)), ], ) : const Text('-', style: TextStyle(color: AppTheme.textSecondary))); default: return const DataCell(SizedBox()); } } } class _StatusBadge extends StatelessWidget { final bool isSoldOut; const _StatusBadge(this.isSoldOut); @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), decoration: BoxDecoration( color: isSoldOut ? AppTheme.textSecondary.withOpacity(0.12) : AppTheme.success.withOpacity(0.12), borderRadius: BorderRadius.circular(3), ), child: Text( isSoldOut ? '已卖出' : '在售', style: TextStyle( fontSize: 12, fontWeight: FontWeight.w600, color: isSoldOut ? AppTheme.textSecondary : AppTheme.success, ), ), ); } } class _OfflineBanner extends StatelessWidget { final VoidCallback onRetry; const _OfflineBanner({required this.onRetry}); @override Widget build(BuildContext context) { return Container( width: double.infinity, color: const Color(0xFFFFF8E1), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6), child: Row( children: [ const Icon(Icons.cloud_off, size: 14, color: Color(0xFFF57F17)), const SizedBox(width: 8), const Expanded( child: Text('网络不可用,当前显示离线缓存数据', style: TextStyle(color: Color(0xFFF57F17), fontSize: 12)), ), TextButton( onPressed: onRetry, style: TextButton.styleFrom( foregroundColor: const Color(0xFFF57F17), padding: const EdgeInsets.symmetric(horizontal: 8)), child: const Text('重试', style: TextStyle(fontSize: 12)), ), ], ), ); } }