feat(client): 初版 Flutter UI 框架,仿照参考截图实现

- 蓝色顶栏(#1565C0)+ 深蓝侧边栏(#0D47A1),含折叠/展开
- 底部状态栏:门店编号、登录用户、登录时间、实时时钟、版本号
- 登录页:居中卡片,支持密码显示/隐藏
- 入库单列表:筛选/搜索/日期选择/分页,三个 Tab(入库单/查询/审核)
- 新建入库单:基本信息 + 商品明细动态增删,实时计算合计金额
- 出库单列表:同入库单风格
- 库存查询:4 张统计卡片 + 颜色高亮缺货/预警行,库存预警 Tab
- 往来单位、财务、商品、系统设置(用户/仓库/编号规则/系统参数)
- 使用 Riverpod 状态管理 + go_router 路由 + Dio HTTP

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-04-04 12:51:41 +08:00
parent d6397083c0
commit 76ab19ea47
22 changed files with 6386 additions and 0 deletions
@@ -0,0 +1,490 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../../core/theme/app_theme.dart';
class InventoryCheckScreen extends ConsumerStatefulWidget {
const InventoryCheckScreen({super.key});
@override
ConsumerState<InventoryCheckScreen> createState() =>
_InventoryCheckScreenState();
}
class _InventoryCheckScreenState
extends ConsumerState<InventoryCheckScreen> {
final _checkNoCtrl =
TextEditingController(text: 'PD20260404001');
String _warehouse = '主仓库';
String _checkType = '全盘';
bool _submitting = false;
// Mock inventory items for checking
final List<Map<String, dynamic>> _checkItems = [
{
'code': 'SP001',
'name': '茅台酒(飞天)53度500ml',
'unit': '',
'systemQty': 286,
'actualQtyCtrl': TextEditingController(text: '286'),
'remark': TextEditingController(),
},
{
'code': 'SP002',
'name': '五粮液(普五)52度500ml',
'unit': '',
'systemQty': 152,
'actualQtyCtrl': TextEditingController(text: '150'),
'remark': TextEditingController(text: '破损2瓶'),
},
{
'code': 'SP003',
'name': '洋河梦之蓝M6+ 45度500ml',
'unit': '',
'systemQty': 88,
'actualQtyCtrl': TextEditingController(text: '88'),
'remark': TextEditingController(),
},
{
'code': 'SP005',
'name': '泸州老窖(国窖157352度500ml',
'unit': '',
'systemQty': 68,
'actualQtyCtrl': TextEditingController(text: '70'),
'remark': TextEditingController(text: '盘盈2瓶'),
},
{
'code': 'SP006',
'name': '汾酒(青花3053度500ml',
'unit': '',
'systemQty': 45,
'actualQtyCtrl': TextEditingController(text: '45'),
'remark': TextEditingController(),
},
{
'code': 'SP010',
'name': '青岛啤酒(经典)500ml',
'unit': '',
'systemQty': 35,
'actualQtyCtrl': TextEditingController(text: '35'),
'remark': TextEditingController(),
},
];
@override
void dispose() {
_checkNoCtrl.dispose();
for (final item in _checkItems) {
(item['actualQtyCtrl'] as TextEditingController).dispose();
(item['remark'] as TextEditingController).dispose();
}
super.dispose();
}
int _getDiff(Map<String, dynamic> item) {
final actual = int.tryParse(
(item['actualQtyCtrl'] as TextEditingController).text) ??
0;
return actual - (item['systemQty'] as int);
}
Future<void> _submit() async {
setState(() => _submitting = true);
await Future.delayed(const Duration(milliseconds: 800));
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('盘点单已提交,待审核'),
backgroundColor: AppTheme.success,
),
);
context.go('/inventory');
}
if (mounted) setState(() => _submitting = false);
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppTheme.background,
body: Column(
children: [
// Header
Container(
height: 52,
color: AppTheme.surface,
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
children: [
IconButton(
icon: const Icon(Icons.arrow_back, size: 20),
onPressed: () => context.go('/inventory'),
),
const SizedBox(width: 8),
const Text('库存盘点',
style: TextStyle(
fontSize: 16, fontWeight: FontWeight.w600)),
const Spacer(),
OutlinedButton(
onPressed: () {},
child: const Text('保存草稿'),
),
const SizedBox(width: 8),
ElevatedButton.icon(
onPressed: _submitting ? null : _submit,
icon: _submitting
? const SizedBox(
width: 14,
height: 14,
child: CircularProgressIndicator(
strokeWidth: 2, color: Colors.white))
: const Icon(Icons.check_circle_outline, size: 16),
label: const Text('提交盘点'),
),
const SizedBox(width: 8),
OutlinedButton(
onPressed: () => context.go('/inventory'),
child: const Text('取消'),
),
],
),
),
const Divider(height: 1),
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
children: [
// Basic info
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('盘点基本信息',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: AppTheme.primaryDark)),
const SizedBox(height: 16),
Wrap(
spacing: 16,
runSpacing: 16,
children: [
_InfoField(
label: '盘点单号',
child: TextFormField(
controller: _checkNoCtrl,
readOnly: true,
style: const TextStyle(
fontFamily: 'monospace'),
decoration: const InputDecoration(),
),
),
_InfoField(
label: '盘点仓库',
child: DropdownButtonFormField<String>(
value: _warehouse,
items: ['主仓库', '副仓库', '全部仓库']
.map((s) => DropdownMenuItem(
value: s,
child: Text(s,
style: const TextStyle(
fontSize: 13))))
.toList(),
onChanged: (v) =>
setState(() => _warehouse = v!),
decoration: const InputDecoration(),
),
),
_InfoField(
label: '盘点类型',
child: DropdownButtonFormField<String>(
value: _checkType,
items: ['全盘', '抽盘', '循环盘点']
.map((s) => DropdownMenuItem(
value: s,
child: Text(s,
style: const TextStyle(
fontSize: 13))))
.toList(),
onChanged: (v) =>
setState(() => _checkType = v!),
decoration: const InputDecoration(),
),
),
_InfoField(
label: '盘点日期',
child: InputDecorator(
decoration: const InputDecoration(),
child: Text(
'2026-04-04',
style: const TextStyle(fontSize: 13),
),
),
),
],
),
],
),
),
),
const SizedBox(height: 12),
// Check items table
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Text('盘点明细',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: AppTheme.primaryDark)),
const SizedBox(width: 12),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: AppTheme.accent.withOpacity(0.1),
borderRadius: BorderRadius.circular(3),
),
child: Text(
'差异 ${_checkItems.where((i) => _getDiff(i) != 0).length}',
style: const TextStyle(
fontSize: 12,
color: AppTheme.accent),
),
),
],
),
const SizedBox(height: 12),
// Table
Table(
columnWidths: const {
0: FixedColumnWidth(36),
1: FixedColumnWidth(80),
2: FlexColumnWidth(3),
3: FixedColumnWidth(50),
4: FixedColumnWidth(80),
5: FixedColumnWidth(120),
6: FixedColumnWidth(80),
7: FlexColumnWidth(2),
},
children: [
TableRow(
decoration: const BoxDecoration(
color: Color(0xFFF0F4FF)),
children: [
'序号',
'商品编码',
'商品名称',
'单位',
'账面数量',
'实际数量',
'差异',
'备注',
]
.map((h) => Padding(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 10),
child: Text(h,
style: const TextStyle(
fontSize: 13,
fontWeight:
FontWeight.w600,
color: AppTheme
.primaryDark)),
))
.toList(),
),
...List.generate(
_checkItems.length,
(i) => _buildCheckRow(i)),
],
),
const Divider(height: 1),
// Summary
Padding(
padding: const EdgeInsets.only(top: 12),
child: Row(
children: [
_SummaryItem(
label: '盘点商品',
value: '${_checkItems.length}',
color: AppTheme.primary,
),
const SizedBox(width: 24),
_SummaryItem(
label: '盘盈',
value:
'${_checkItems.where((i) => _getDiff(i) > 0).length}',
color: AppTheme.success,
),
const SizedBox(width: 24),
_SummaryItem(
label: '盘亏',
value:
'${_checkItems.where((i) => _getDiff(i) < 0).length}',
color: AppTheme.danger,
),
const SizedBox(width: 24),
_SummaryItem(
label: '相符',
value:
'${_checkItems.where((i) => _getDiff(i) == 0).length}',
color: AppTheme.textSecondary,
),
],
),
),
],
),
),
),
],
),
),
),
],
),
);
}
TableRow _buildCheckRow(int index) {
final item = _checkItems[index];
final diff = _getDiff(item);
return TableRow(
decoration: BoxDecoration(
color: diff != 0
? (diff > 0
? AppTheme.success.withOpacity(0.04)
: AppTheme.danger.withOpacity(0.04))
: (index.isEven ? Colors.white : const Color(0xFFFAFAFA)),
),
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 10),
child: Text('${index + 1}',
style: const TextStyle(
fontSize: 13, color: AppTheme.textSecondary)),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 10),
child: Text(item['code'] as String,
style: const TextStyle(
fontFamily: 'monospace',
fontSize: 12,
color: AppTheme.textSecondary)),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 10),
child: Text(item['name'] as String,
style: const TextStyle(fontSize: 13),
overflow: TextOverflow.ellipsis),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 10),
child: Text(item['unit'] as String,
style: const TextStyle(fontSize: 13)),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 10),
child: Text('${item['systemQty']}',
style: const TextStyle(fontSize: 13)),
),
Padding(
padding: const EdgeInsets.all(4),
child: TextFormField(
controller: item['actualQtyCtrl'] as TextEditingController,
decoration: const InputDecoration(),
style: const TextStyle(fontSize: 13),
keyboardType: TextInputType.number,
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
onChanged: (_) => setState(() {}),
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 10),
child: Text(
diff == 0 ? '0' : (diff > 0 ? '+$diff' : '$diff'),
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: diff == 0
? AppTheme.textSecondary
: (diff > 0 ? AppTheme.success : AppTheme.danger),
),
),
),
Padding(
padding: const EdgeInsets.all(4),
child: TextFormField(
controller: item['remark'] as TextEditingController,
decoration: const InputDecoration(hintText: '备注'),
style: const TextStyle(fontSize: 13),
),
),
],
);
}
}
class _InfoField extends StatelessWidget {
final String label;
final Widget child;
const _InfoField({required this.label, required this.child});
@override
Widget build(BuildContext context) {
return SizedBox(
width: 220,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(label,
style: const TextStyle(
fontSize: 13, color: AppTheme.textSecondary)),
const SizedBox(height: 6),
child,
],
),
);
}
}
class _SummaryItem extends StatelessWidget {
final String label;
final String value;
final Color color;
const _SummaryItem(
{required this.label,
required this.value,
required this.color});
@override
Widget build(BuildContext context) {
return Row(
children: [
Text(label,
style: const TextStyle(
fontSize: 13, color: AppTheme.textSecondary)),
const SizedBox(width: 4),
Text(value,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: color)),
],
);
}
}
@@ -0,0 +1,700 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../../widgets/page_scaffold.dart';
import '../../widgets/data_table_card.dart';
import '../../core/theme/app_theme.dart';
class InventoryListScreen extends ConsumerStatefulWidget {
const InventoryListScreen({super.key});
@override
ConsumerState<InventoryListScreen> createState() =>
_InventoryListScreenState();
}
class _InventoryListScreenState extends ConsumerState<InventoryListScreen> {
int _page = 1;
final _searchCtrl = TextEditingController();
String _categoryFilter = '全部';
String _warehouseFilter = '全部';
final List<Map<String, dynamic>> _mockInventory = [
{
'code': 'SP001',
'name': '茅台酒(飞天)53度500ml',
'category': '白酒',
'brand': '茅台',
'spec': '500ml/瓶',
'unit': '',
'warehouse': '主仓库',
'qty': 286,
'available': 280,
'reserved': 6,
'cost': 2100.00,
'price': 2600.00,
'totalValue': '600600.00',
'minQty': 50,
'status': '正常',
},
{
'code': 'SP002',
'name': '五粮液(普五)52度500ml',
'category': '白酒',
'brand': '五粮液',
'spec': '500ml/瓶',
'unit': '',
'warehouse': '主仓库',
'qty': 152,
'available': 148,
'reserved': 4,
'cost': 850.00,
'price': 1050.00,
'totalValue': '129200.00',
'minQty': 30,
'status': '正常',
},
{
'code': 'SP003',
'name': '洋河梦之蓝M6+ 45度500ml',
'category': '白酒',
'brand': '洋河',
'spec': '500ml/瓶',
'unit': '',
'warehouse': '主仓库',
'qty': 88,
'available': 85,
'reserved': 3,
'cost': 480.00,
'price': 598.00,
'totalValue': '42240.00',
'minQty': 20,
'status': '正常',
},
{
'code': 'SP004',
'name': '剑南春(水晶剑)52度500ml',
'category': '白酒',
'brand': '剑南春',
'spec': '500ml/瓶',
'unit': '',
'warehouse': '副仓库',
'qty': 12,
'available': 12,
'reserved': 0,
'cost': 288.00,
'price': 368.00,
'totalValue': '3456.00',
'minQty': 20,
'status': '库存不足',
},
{
'code': 'SP005',
'name': '泸州老窖(国窖157352度500ml',
'category': '白酒',
'brand': '泸州老窖',
'spec': '500ml/瓶',
'unit': '',
'warehouse': '主仓库',
'qty': 68,
'available': 65,
'reserved': 3,
'cost': 680.00,
'price': 860.00,
'totalValue': '46240.00',
'minQty': 20,
'status': '正常',
},
{
'code': 'SP006',
'name': '汾酒(青花3053度500ml',
'category': '白酒',
'brand': '汾酒',
'spec': '500ml/瓶',
'unit': '',
'warehouse': '主仓库',
'qty': 45,
'available': 45,
'reserved': 0,
'cost': 320.00,
'price': 418.00,
'totalValue': '14400.00',
'minQty': 15,
'status': '正常',
},
{
'code': 'SP007',
'name': '拉菲古堡正牌红葡萄酒2018',
'category': '葡萄酒',
'brand': '拉菲',
'spec': '750ml/瓶',
'unit': '',
'warehouse': '副仓库',
'qty': 24,
'available': 24,
'reserved': 0,
'cost': 5200.00,
'price': 6800.00,
'totalValue': '124800.00',
'minQty': 6,
'status': '正常',
},
{
'code': 'SP008',
'name': '人头马XO特优香槟干邑700ml',
'category': '洋酒',
'brand': '人头马',
'spec': '700ml/瓶',
'unit': '',
'warehouse': '副仓库',
'qty': 18,
'available': 16,
'reserved': 2,
'cost': 1680.00,
'price': 2180.00,
'totalValue': '30240.00',
'minQty': 6,
'status': '正常',
},
{
'code': 'SP009',
'name': '百威啤酒330ml',
'category': '啤酒',
'brand': '百威',
'spec': '330ml×24罐',
'unit': '',
'warehouse': '主仓库',
'qty': 8,
'available': 6,
'reserved': 2,
'cost': 58.00,
'price': 88.00,
'totalValue': '464.00',
'minQty': 20,
'status': '库存不足',
},
{
'code': 'SP010',
'name': '青岛啤酒(经典)500ml',
'category': '啤酒',
'brand': '青岛',
'spec': '500ml×12瓶',
'unit': '',
'warehouse': '主仓库',
'qty': 35,
'available': 35,
'reserved': 0,
'cost': 42.00,
'price': 68.00,
'totalValue': '1470.00',
'minQty': 20,
'status': '正常',
},
{
'code': 'SP011',
'name': '芝华士12年苏格兰威士忌700ml',
'category': '洋酒',
'brand': '芝华士',
'spec': '700ml/瓶',
'unit': '',
'warehouse': '副仓库',
'qty': 30,
'available': 28,
'reserved': 2,
'cost': 288.00,
'price': 398.00,
'totalValue': '8640.00',
'minQty': 10,
'status': '正常',
},
{
'code': 'SP012',
'name': '郎酒红花郎15年53度500ml',
'category': '白酒',
'brand': '郎酒',
'spec': '500ml/瓶',
'unit': '',
'warehouse': '主仓库',
'qty': 0,
'available': 0,
'reserved': 0,
'cost': 620.00,
'price': 798.00,
'totalValue': '0.00',
'minQty': 10,
'status': '缺货',
},
];
@override
void dispose() {
_searchCtrl.dispose();
super.dispose();
}
List<Map<String, dynamic>> get _filtered {
return _mockInventory.where((item) {
if (_categoryFilter != '全部' &&
item['category'] != _categoryFilter) return false;
if (_warehouseFilter != '全部' &&
item['warehouse'] != _warehouseFilter) return false;
final q = _searchCtrl.text.toLowerCase();
if (q.isNotEmpty) {
final name = (item['name'] as String).toLowerCase();
final code = (item['code'] as String).toLowerCase();
final brand = (item['brand'] as String).toLowerCase();
if (!name.contains(q) && !code.contains(q) && !brand.contains(q)) {
return false;
}
}
return true;
}).toList();
}
// Summary stats
double get _totalInventoryValue {
return _mockInventory.fold(0, (sum, item) {
return sum + (double.tryParse(item['totalValue'] as String) ?? 0);
});
}
int get _lowStockCount {
return _mockInventory.where((item) {
return (item['qty'] as int) < (item['minQty'] as int);
}).length;
}
@override
Widget build(BuildContext context) {
return PageScaffold(
title: '库存管理',
tabs: const [
Tab(text: '库存查询'),
Tab(text: '库存预警'),
Tab(text: '库存盘点'),
],
tabViews: [
_buildInventoryList(),
_buildWarningList(),
_buildCheckTab(),
],
);
}
Widget _buildInventoryList() {
final items = _filtered;
return Column(
children: [
// Summary cards
Container(
color: AppTheme.background,
padding: const EdgeInsets.all(12),
child: Row(
children: [
_SummaryCard(
title: '商品总数',
value: '${_mockInventory.length}',
unit: '',
icon: Icons.inventory_2,
color: AppTheme.primary),
const SizedBox(width: 12),
_SummaryCard(
title: '库存总价值',
value: '¥${(_totalInventoryValue / 10000).toStringAsFixed(1)}',
unit: '',
icon: Icons.monetization_on,
color: AppTheme.success),
const SizedBox(width: 12),
_SummaryCard(
title: '库存预警',
value: '$_lowStockCount',
unit: '',
icon: Icons.warning_amber,
color: AppTheme.accent),
const SizedBox(width: 12),
_SummaryCard(
title: '缺货商品',
value:
'${_mockInventory.where((i) => (i['qty'] as int) == 0).length}',
unit: '',
icon: Icons.remove_shopping_cart,
color: AppTheme.danger),
],
),
),
const Divider(height: 1),
Expanded(
child: DataTableCard(
totalCount: items.length,
page: _page,
onPageChanged: (p) => setState(() => _page = p),
toolbar: Row(
children: [
OutlinedButton.icon(
onPressed: () => context.go('/inventory/check'),
icon: const Icon(Icons.fact_check, size: 16),
label: const Text('发起盘点'),
),
const SizedBox(width: 8),
OutlinedButton.icon(
onPressed: () {},
icon: const Icon(Icons.file_download_outlined, size: 16),
label: const Text('导出'),
),
const Spacer(),
_DropdownFilter(
value: _categoryFilter,
items: ['全部', '白酒', '葡萄酒', '洋酒', '啤酒'],
onChanged: (v) => setState(() => _categoryFilter = v!),
hint: '商品分类'),
const SizedBox(width: 8),
_DropdownFilter(
value: _warehouseFilter,
items: ['全部', '主仓库', '副仓库'],
onChanged: (v) =>
setState(() => _warehouseFilter = v!),
hint: '仓库'),
const SizedBox(width: 8),
SizedBox(
width: 180,
child: TextField(
controller: _searchCtrl,
decoration: const InputDecoration(
hintText: '搜索商品名/编码/品牌',
prefixIcon: Icon(Icons.search, size: 16),
hintStyle: TextStyle(fontSize: 13),
),
onChanged: (_) => setState(() => _page = 1),
),
),
],
),
columns: const [
DataColumn(label: Text('商品编码')),
DataColumn(label: Text('商品名称')),
DataColumn(label: Text('分类')),
DataColumn(label: Text('品牌')),
DataColumn(label: Text('仓库')),
DataColumn(label: Text('库存'), numeric: true),
DataColumn(label: Text('可用'), numeric: true),
DataColumn(label: Text('预留'), numeric: true),
DataColumn(label: Text('成本价'), numeric: true),
DataColumn(label: Text('库存价值'), numeric: true),
DataColumn(label: Text('状态')),
],
rows: items
.map((item) => DataRow(
color: WidgetStateProperty.resolveWith((states) {
if ((item['qty'] as int) == 0) {
return AppTheme.danger.withOpacity(0.04);
}
if ((item['qty'] as int) < (item['minQty'] as int)) {
return AppTheme.accent.withOpacity(0.04);
}
return null;
}),
cells: [
DataCell(Text(item['code'] as String,
style: const TextStyle(
fontFamily: 'monospace',
fontSize: 12,
color: AppTheme.textSecondary))),
DataCell(SizedBox(
width: 200,
child: Text(item['name'] as String,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontSize: 13)),
)),
DataCell(Text(item['category'] as String)),
DataCell(Text(item['brand'] as String)),
DataCell(Text(item['warehouse'] as String)),
DataCell(Text('${item['qty']}',
style: TextStyle(
fontWeight: FontWeight.w600,
color: (item['qty'] as int) == 0
? AppTheme.danger
: (item['qty'] as int) <
(item['minQty'] as int)
? AppTheme.accent
: AppTheme.textPrimary))),
DataCell(Text('${item['available']}')),
DataCell(Text('${item['reserved']}',
style: TextStyle(
color: (item['reserved'] as int) > 0
? AppTheme.accent
: AppTheme.textSecondary))),
DataCell(Text(
'¥${(item['cost'] as double).toStringAsFixed(2)}')),
DataCell(Text('¥${item['totalValue']}',
style: const TextStyle(
fontWeight: FontWeight.w500))),
DataCell(_InventoryStatusBadge(
item['status'] as String)),
],
))
.toList(),
),
),
],
);
}
Widget _buildWarningList() {
final warnings = _mockInventory
.where((item) => (item['qty'] as int) < (item['minQty'] as int))
.toList();
return DataTableCard(
totalCount: warnings.length,
page: 1,
toolbar: Row(
children: [
const Icon(Icons.warning_amber, color: AppTheme.accent, size: 18),
const SizedBox(width: 8),
Text(
'${warnings.length} 个商品库存低于安全库存',
style: const TextStyle(
fontSize: 13, color: AppTheme.accent),
),
const Spacer(),
ElevatedButton.icon(
onPressed: () {},
icon: const Icon(Icons.mail_outline, size: 16),
label: const Text('发送预警通知'),
),
const SizedBox(width: 8),
OutlinedButton.icon(
onPressed: () {},
icon: const Icon(Icons.shopping_cart_checkout, size: 16),
label: const Text('一键补货申请'),
),
],
),
columns: const [
DataColumn(label: Text('商品编码')),
DataColumn(label: Text('商品名称')),
DataColumn(label: Text('分类')),
DataColumn(label: Text('仓库')),
DataColumn(label: Text('当前库存'), numeric: true),
DataColumn(label: Text('安全库存'), numeric: true),
DataColumn(label: Text('缺口'), numeric: true),
DataColumn(label: Text('状态')),
DataColumn(label: Text('操作')),
],
rows: warnings
.map((item) => DataRow(
color: WidgetStateProperty.all(
(item['qty'] as int) == 0
? AppTheme.danger.withOpacity(0.05)
: AppTheme.accent.withOpacity(0.04)),
cells: [
DataCell(Text(item['code'] as String,
style: const TextStyle(
fontFamily: 'monospace', fontSize: 12))),
DataCell(SizedBox(
width: 180,
child: Text(item['name'] as String,
overflow: TextOverflow.ellipsis),
)),
DataCell(Text(item['category'] as String)),
DataCell(Text(item['warehouse'] as String)),
DataCell(Text('${item['qty']}',
style: TextStyle(
fontWeight: FontWeight.w700,
color: (item['qty'] as int) == 0
? AppTheme.danger
: AppTheme.accent))),
DataCell(Text('${item['minQty']}')),
DataCell(Text(
'${(item['minQty'] as int) - (item['qty'] as int)}',
style: const TextStyle(
color: AppTheme.danger,
fontWeight: FontWeight.w600))),
DataCell(_InventoryStatusBadge(item['status'] as String)),
DataCell(
ElevatedButton(
onPressed: () {},
style: ElevatedButton.styleFrom(
minimumSize: const Size(0, 28)),
child: const Text('申请补货',
style: TextStyle(fontSize: 12)),
),
),
],
))
.toList(),
);
}
Widget _buildCheckTab() {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.fact_check_outlined,
size: 64, color: AppTheme.textSecondary),
const SizedBox(height: 16),
const Text('点击下方按钮发起新的库存盘点',
style: TextStyle(fontSize: 15, color: AppTheme.textSecondary)),
const SizedBox(height: 24),
ElevatedButton.icon(
onPressed: () => context.go('/inventory/check'),
icon: const Icon(Icons.add, size: 20),
label: const Text('新建盘点单', style: TextStyle(fontSize: 15)),
style: ElevatedButton.styleFrom(
minimumSize: const Size(160, 44)),
),
],
),
);
}
}
class _SummaryCard extends StatelessWidget {
final String title;
final String value;
final String unit;
final IconData icon;
final Color color;
const _SummaryCard({
required this.title,
required this.value,
required this.unit,
required this.icon,
required this.color,
});
@override
Widget build(BuildContext context) {
return Expanded(
child: Container(
height: 72,
padding: const EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration(
color: AppTheme.surface,
borderRadius: BorderRadius.circular(4),
border: Border.all(color: AppTheme.border, width: 0.5),
),
child: Row(
children: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(icon, color: color, size: 22),
),
const SizedBox(width: 12),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(title,
style: const TextStyle(
fontSize: 12, color: AppTheme.textSecondary)),
const SizedBox(height: 4),
Row(
crossAxisAlignment: CrossAxisAlignment.baseline,
textBaseline: TextBaseline.alphabetic,
children: [
Text(value,
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w700,
color: color)),
if (unit.isNotEmpty) ...[
const SizedBox(width: 2),
Text(unit,
style: const TextStyle(
fontSize: 12,
color: AppTheme.textSecondary)),
],
],
),
],
),
],
),
),
);
}
}
class _InventoryStatusBadge extends StatelessWidget {
final String status;
const _InventoryStatusBadge(this.status);
@override
Widget build(BuildContext context) {
final Color bg;
final Color fg;
switch (status) {
case '正常':
bg = const Color(0xFFE8F5E9);
fg = AppTheme.success;
break;
case '库存不足':
bg = const Color(0xFFFFF3E0);
fg = AppTheme.accent;
break;
case '缺货':
bg = const Color(0xFFFFEBEE);
fg = AppTheme.danger;
break;
default:
bg = const Color(0xFFF5F5F5);
fg = AppTheme.textSecondary;
}
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: bg, borderRadius: BorderRadius.circular(3)),
child: Text(status,
style: TextStyle(
color: fg, fontSize: 12, fontWeight: FontWeight.w500)),
);
}
}
class _DropdownFilter extends StatelessWidget {
final String value;
final List<String> items;
final ValueChanged<String?> onChanged;
final String hint;
const _DropdownFilter({
required this.value,
required this.items,
required this.onChanged,
required this.hint,
});
@override
Widget build(BuildContext context) {
return Container(
height: 36,
padding: const EdgeInsets.symmetric(horizontal: 8),
decoration: BoxDecoration(
border: Border.all(color: AppTheme.border),
borderRadius: BorderRadius.circular(4),
color: AppTheme.surface,
),
child: DropdownButtonHideUnderline(
child: DropdownButton<String>(
value: value,
items: items
.map((s) => DropdownMenuItem(
value: s,
child: Text(s, style: const TextStyle(fontSize: 13))))
.toList(),
onChanged: onChanged,
style: const TextStyle(
fontSize: 13, color: AppTheme.textPrimary),
),
),
);
}
}