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:
@@ -0,0 +1,758 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../../widgets/page_scaffold.dart';
|
||||
import '../../widgets/data_table_card.dart';
|
||||
import '../../core/theme/app_theme.dart';
|
||||
import '../../widgets/form_dialog.dart';
|
||||
|
||||
class ProductsScreen extends ConsumerStatefulWidget {
|
||||
const ProductsScreen({super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<ProductsScreen> createState() => _ProductsScreenState();
|
||||
}
|
||||
|
||||
class _ProductsScreenState extends ConsumerState<ProductsScreen> {
|
||||
int _productsPage = 1;
|
||||
int _categoriesPage = 1;
|
||||
int _warehousesPage = 1;
|
||||
final _searchCtrl = TextEditingController();
|
||||
String _categoryFilter = '全部';
|
||||
|
||||
final List<Map<String, dynamic>> _products = [
|
||||
{
|
||||
'code': 'SP001',
|
||||
'name': '茅台酒(飞天)53度500ml',
|
||||
'category': '白酒',
|
||||
'brand': '茅台',
|
||||
'spec': '500ml/瓶',
|
||||
'unit': '瓶',
|
||||
'barcode': '6901735001534',
|
||||
'costPrice': '2100.00',
|
||||
'salePrice': '2600.00',
|
||||
'minStock': 50,
|
||||
'maxStock': 500,
|
||||
'status': '启用',
|
||||
},
|
||||
{
|
||||
'code': 'SP002',
|
||||
'name': '五粮液(普五)52度500ml',
|
||||
'category': '白酒',
|
||||
'brand': '五粮液',
|
||||
'spec': '500ml/瓶',
|
||||
'unit': '瓶',
|
||||
'barcode': '6901234567890',
|
||||
'costPrice': '850.00',
|
||||
'salePrice': '1050.00',
|
||||
'minStock': 30,
|
||||
'maxStock': 300,
|
||||
'status': '启用',
|
||||
},
|
||||
{
|
||||
'code': 'SP003',
|
||||
'name': '洋河梦之蓝M6+ 45度500ml',
|
||||
'category': '白酒',
|
||||
'brand': '洋河',
|
||||
'spec': '500ml/瓶',
|
||||
'unit': '瓶',
|
||||
'barcode': '6902701234567',
|
||||
'costPrice': '480.00',
|
||||
'salePrice': '598.00',
|
||||
'minStock': 20,
|
||||
'maxStock': 200,
|
||||
'status': '启用',
|
||||
},
|
||||
{
|
||||
'code': 'SP004',
|
||||
'name': '剑南春(水晶剑)52度500ml',
|
||||
'category': '白酒',
|
||||
'brand': '剑南春',
|
||||
'spec': '500ml/瓶',
|
||||
'unit': '瓶',
|
||||
'barcode': '6921234567890',
|
||||
'costPrice': '288.00',
|
||||
'salePrice': '368.00',
|
||||
'minStock': 20,
|
||||
'maxStock': 200,
|
||||
'status': '启用',
|
||||
},
|
||||
{
|
||||
'code': 'SP005',
|
||||
'name': '泸州老窖(国窖1573)52度500ml',
|
||||
'category': '白酒',
|
||||
'brand': '泸州老窖',
|
||||
'spec': '500ml/瓶',
|
||||
'unit': '瓶',
|
||||
'barcode': '6910123456789',
|
||||
'costPrice': '680.00',
|
||||
'salePrice': '860.00',
|
||||
'minStock': 20,
|
||||
'maxStock': 200,
|
||||
'status': '启用',
|
||||
},
|
||||
{
|
||||
'code': 'SP006',
|
||||
'name': '汾酒(青花30)53度500ml',
|
||||
'category': '白酒',
|
||||
'brand': '汾酒',
|
||||
'spec': '500ml/瓶',
|
||||
'unit': '瓶',
|
||||
'barcode': '6936789012345',
|
||||
'costPrice': '320.00',
|
||||
'salePrice': '418.00',
|
||||
'minStock': 15,
|
||||
'maxStock': 150,
|
||||
'status': '启用',
|
||||
},
|
||||
{
|
||||
'code': 'SP007',
|
||||
'name': '拉菲古堡正牌红葡萄酒2018',
|
||||
'category': '葡萄酒',
|
||||
'brand': '拉菲',
|
||||
'spec': '750ml/瓶',
|
||||
'unit': '瓶',
|
||||
'barcode': '3760040234567',
|
||||
'costPrice': '5200.00',
|
||||
'salePrice': '6800.00',
|
||||
'minStock': 6,
|
||||
'maxStock': 60,
|
||||
'status': '启用',
|
||||
},
|
||||
{
|
||||
'code': 'SP008',
|
||||
'name': '人头马XO特优香槟干邑700ml',
|
||||
'category': '洋酒',
|
||||
'brand': '人头马',
|
||||
'spec': '700ml/瓶',
|
||||
'unit': '瓶',
|
||||
'barcode': '5010677012345',
|
||||
'costPrice': '1680.00',
|
||||
'salePrice': '2180.00',
|
||||
'minStock': 6,
|
||||
'maxStock': 60,
|
||||
'status': '启用',
|
||||
},
|
||||
{
|
||||
'code': 'SP009',
|
||||
'name': '百威啤酒330ml×24罐',
|
||||
'category': '啤酒',
|
||||
'brand': '百威',
|
||||
'spec': '330ml×24罐/箱',
|
||||
'unit': '箱',
|
||||
'barcode': '6901234000123',
|
||||
'costPrice': '58.00',
|
||||
'salePrice': '88.00',
|
||||
'minStock': 20,
|
||||
'maxStock': 200,
|
||||
'status': '启用',
|
||||
},
|
||||
{
|
||||
'code': 'SP010',
|
||||
'name': '青岛啤酒(经典)500ml×12瓶',
|
||||
'category': '啤酒',
|
||||
'brand': '青岛',
|
||||
'spec': '500ml×12瓶/箱',
|
||||
'unit': '箱',
|
||||
'barcode': '6901234000456',
|
||||
'costPrice': '42.00',
|
||||
'salePrice': '68.00',
|
||||
'minStock': 20,
|
||||
'maxStock': 200,
|
||||
'status': '启用',
|
||||
},
|
||||
{
|
||||
'code': 'SP011',
|
||||
'name': '芝华士12年苏格兰威士忌700ml',
|
||||
'category': '洋酒',
|
||||
'brand': '芝华士',
|
||||
'spec': '700ml/瓶',
|
||||
'unit': '瓶',
|
||||
'barcode': '5000299606230',
|
||||
'costPrice': '288.00',
|
||||
'salePrice': '398.00',
|
||||
'minStock': 10,
|
||||
'maxStock': 100,
|
||||
'status': '启用',
|
||||
},
|
||||
{
|
||||
'code': 'SP012',
|
||||
'name': '郎酒红花郎15年53度500ml',
|
||||
'category': '白酒',
|
||||
'brand': '郎酒',
|
||||
'spec': '500ml/瓶',
|
||||
'unit': '瓶',
|
||||
'barcode': '6914987012345',
|
||||
'costPrice': '620.00',
|
||||
'salePrice': '798.00',
|
||||
'minStock': 10,
|
||||
'maxStock': 100,
|
||||
'status': '禁用',
|
||||
},
|
||||
];
|
||||
|
||||
final List<Map<String, dynamic>> _categories = [
|
||||
{'code': 'BJ', 'name': '白酒', 'parent': '酒类', 'count': 7, 'status': '启用'},
|
||||
{'code': 'PTJ', 'name': '葡萄酒', 'parent': '酒类', 'count': 1, 'status': '启用'},
|
||||
{'code': 'YJ', 'name': '洋酒', 'parent': '酒类', 'count': 2, 'status': '启用'},
|
||||
{'code': 'PJ', 'name': '啤酒', 'parent': '酒类', 'count': 2, 'status': '启用'},
|
||||
{'code': 'HJ', 'name': '黄酒', 'parent': '酒类', 'count': 0, 'status': '启用'},
|
||||
{'code': 'MLJ', 'name': '米露酒', 'parent': '酒类', 'count': 0, 'status': '启用'},
|
||||
];
|
||||
|
||||
final List<Map<String, dynamic>> _warehouses = [
|
||||
{
|
||||
'code': 'CK001',
|
||||
'name': '主仓库',
|
||||
'type': '常规仓',
|
||||
'location': 'B1层西区',
|
||||
'capacity': 1000,
|
||||
'used': 720,
|
||||
'manager': '仓库主任',
|
||||
'status': '启用',
|
||||
},
|
||||
{
|
||||
'code': 'CK002',
|
||||
'name': '副仓库',
|
||||
'type': '常规仓',
|
||||
'location': 'B1层东区',
|
||||
'capacity': 500,
|
||||
'used': 150,
|
||||
'manager': '仓库副主任',
|
||||
'status': '启用',
|
||||
},
|
||||
{
|
||||
'code': 'CK003',
|
||||
'name': '保税仓库',
|
||||
'type': '保税仓',
|
||||
'location': 'B2层',
|
||||
'capacity': 200,
|
||||
'used': 0,
|
||||
'manager': '待指派',
|
||||
'status': '禁用',
|
||||
},
|
||||
];
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_searchCtrl.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
List<Map<String, dynamic>> get _filteredProducts {
|
||||
return _products.where((p) {
|
||||
if (_categoryFilter != '全部' && p['category'] != _categoryFilter) {
|
||||
return false;
|
||||
}
|
||||
final q = _searchCtrl.text.toLowerCase();
|
||||
if (q.isNotEmpty) {
|
||||
final name = (p['name'] as String).toLowerCase();
|
||||
final code = (p['code'] as String).toLowerCase();
|
||||
final brand = (p['brand'] as String).toLowerCase();
|
||||
if (!name.contains(q) && !code.contains(q) && !brand.contains(q)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}).toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return PageScaffold(
|
||||
title: '基础数据',
|
||||
tabs: const [
|
||||
Tab(text: '商品档案'),
|
||||
Tab(text: '商品分类'),
|
||||
Tab(text: '仓库管理'),
|
||||
],
|
||||
tabViews: [
|
||||
_buildProductList(),
|
||||
_buildCategoryList(),
|
||||
_buildWarehouseList(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildProductList() {
|
||||
final products = _filteredProducts;
|
||||
return DataTableCard(
|
||||
totalCount: products.length,
|
||||
page: _productsPage,
|
||||
onPageChanged: (p) => setState(() => _productsPage = p),
|
||||
toolbar: Row(
|
||||
children: [
|
||||
ElevatedButton.icon(
|
||||
onPressed: () => _showProductDialog(context),
|
||||
icon: const Icon(Icons.add, size: 16),
|
||||
label: const Text('新增商品'),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
OutlinedButton.icon(
|
||||
onPressed: () {},
|
||||
icon: const Icon(Icons.file_upload_outlined, 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),
|
||||
SizedBox(
|
||||
width: 200,
|
||||
child: TextField(
|
||||
controller: _searchCtrl,
|
||||
decoration: const InputDecoration(
|
||||
hintText: '搜索商品名/编码/品牌',
|
||||
prefixIcon: Icon(Icons.search, size: 16),
|
||||
hintStyle: TextStyle(fontSize: 13),
|
||||
),
|
||||
onChanged: (_) => setState(() {}),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
columns: const [
|
||||
DataColumn(label: Text('商品编码')),
|
||||
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('状态')),
|
||||
DataColumn(label: Text('操作')),
|
||||
],
|
||||
rows: products
|
||||
.map((p) => DataRow(
|
||||
color: WidgetStateProperty.resolveWith((_) =>
|
||||
p['status'] == '禁用'
|
||||
? AppTheme.textSecondary.withOpacity(0.04)
|
||||
: null),
|
||||
cells: [
|
||||
DataCell(Text(p['code'] as String,
|
||||
style: const TextStyle(
|
||||
fontFamily: 'monospace',
|
||||
fontSize: 12,
|
||||
color: AppTheme.textSecondary))),
|
||||
DataCell(SizedBox(
|
||||
width: 180,
|
||||
child: Text(p['name'] as String,
|
||||
overflow: TextOverflow.ellipsis),
|
||||
)),
|
||||
DataCell(Text(p['category'] as String)),
|
||||
DataCell(Text(p['brand'] as String)),
|
||||
DataCell(Text(p['spec'] as String)),
|
||||
DataCell(Text(p['unit'] as String)),
|
||||
DataCell(Text('¥${p['costPrice']}')),
|
||||
DataCell(Text('¥${p['salePrice']}',
|
||||
style: const TextStyle(color: AppTheme.primary))),
|
||||
DataCell(Text('${p['minStock']}')),
|
||||
DataCell(_StatusBadge(p['status'] as String)),
|
||||
DataCell(Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () {},
|
||||
child: const Text('编辑',
|
||||
style: TextStyle(fontSize: 12))),
|
||||
TextButton(
|
||||
onPressed: () => setState(() {
|
||||
p['status'] = p['status'] == '启用' ? '禁用' : '启用';
|
||||
}),
|
||||
child: Text(
|
||||
p['status'] == '启用' ? '禁用' : '启用',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: p['status'] == '启用'
|
||||
? AppTheme.danger
|
||||
: AppTheme.success),
|
||||
)),
|
||||
],
|
||||
)),
|
||||
],
|
||||
))
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCategoryList() {
|
||||
return DataTableCard(
|
||||
totalCount: _categories.length,
|
||||
page: _categoriesPage,
|
||||
onPageChanged: (p) => setState(() => _categoriesPage = p),
|
||||
toolbar: Row(
|
||||
children: [
|
||||
ElevatedButton.icon(
|
||||
onPressed: () {},
|
||||
icon: const Icon(Icons.add, size: 16),
|
||||
label: const Text('新增分类'),
|
||||
),
|
||||
],
|
||||
),
|
||||
columns: const [
|
||||
DataColumn(label: Text('分类编码')),
|
||||
DataColumn(label: Text('分类名称')),
|
||||
DataColumn(label: Text('上级分类')),
|
||||
DataColumn(label: Text('商品数量'), numeric: true),
|
||||
DataColumn(label: Text('状态')),
|
||||
DataColumn(label: Text('操作')),
|
||||
],
|
||||
rows: _categories
|
||||
.map((c) => DataRow(cells: [
|
||||
DataCell(Text(c['code'] as String,
|
||||
style: const TextStyle(fontFamily: 'monospace', fontSize: 12))),
|
||||
DataCell(Text(c['name'] as String,
|
||||
style: const TextStyle(fontWeight: FontWeight.w500))),
|
||||
DataCell(Text(c['parent'] as String)),
|
||||
DataCell(Text('${c['count']}')),
|
||||
DataCell(_StatusBadge(c['status'] as String)),
|
||||
DataCell(Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () {},
|
||||
child: const Text('编辑',
|
||||
style: TextStyle(fontSize: 12))),
|
||||
if ((c['count'] as int) == 0)
|
||||
TextButton(
|
||||
onPressed: () {},
|
||||
child: const Text('删除',
|
||||
style: TextStyle(
|
||||
fontSize: 12, color: AppTheme.danger))),
|
||||
],
|
||||
)),
|
||||
]))
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildWarehouseList() {
|
||||
return DataTableCard(
|
||||
totalCount: _warehouses.length,
|
||||
page: _warehousesPage,
|
||||
onPageChanged: (p) => setState(() => _warehousesPage = p),
|
||||
toolbar: Row(
|
||||
children: [
|
||||
ElevatedButton.icon(
|
||||
onPressed: () {},
|
||||
icon: const Icon(Icons.add, 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('状态')),
|
||||
DataColumn(label: Text('操作')),
|
||||
],
|
||||
rows: _warehouses
|
||||
.map((w) {
|
||||
final usageRate =
|
||||
((w['used'] as int) / (w['capacity'] as int) * 100)
|
||||
.toStringAsFixed(1);
|
||||
return DataRow(cells: [
|
||||
DataCell(Text(w['code'] as String,
|
||||
style: const TextStyle(fontFamily: 'monospace', fontSize: 12))),
|
||||
DataCell(Text(w['name'] as String,
|
||||
style: const TextStyle(fontWeight: FontWeight.w500))),
|
||||
DataCell(Text(w['type'] as String)),
|
||||
DataCell(Text(w['location'] as String)),
|
||||
DataCell(Text('${w['capacity']}')),
|
||||
DataCell(Text('${w['used']}')),
|
||||
DataCell(Stack(
|
||||
alignment: Alignment.centerLeft,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 60,
|
||||
child: LinearProgressIndicator(
|
||||
value: (w['used'] as int) / (w['capacity'] as int),
|
||||
backgroundColor: AppTheme.border,
|
||||
color: (w['used'] as int) / (w['capacity'] as int) > 0.8
|
||||
? AppTheme.danger
|
||||
: AppTheme.primary,
|
||||
minHeight: 6,
|
||||
borderRadius: BorderRadius.circular(3),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 12),
|
||||
child: Text('$usageRate%',
|
||||
style: const TextStyle(fontSize: 11)),
|
||||
),
|
||||
],
|
||||
)),
|
||||
DataCell(Text(w['manager'] as String)),
|
||||
DataCell(_StatusBadge(w['status'] as String)),
|
||||
DataCell(Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () {},
|
||||
child: const Text('编辑',
|
||||
style: TextStyle(fontSize: 12))),
|
||||
],
|
||||
)),
|
||||
]);
|
||||
})
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
|
||||
void _showProductDialog(BuildContext context) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (ctx) => FormDialog(
|
||||
title: '新增商品',
|
||||
width: 600,
|
||||
onConfirm: () {
|
||||
Navigator.of(ctx).pop();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('商品添加成功'),
|
||||
backgroundColor: AppTheme.success,
|
||||
),
|
||||
);
|
||||
},
|
||||
content: Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _Field(
|
||||
label: '商品名称',
|
||||
required: true,
|
||||
child: TextFormField(
|
||||
decoration:
|
||||
const InputDecoration(hintText: '请输入商品全称')),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: _Field(
|
||||
label: '商品分类',
|
||||
required: true,
|
||||
child: DropdownButtonFormField<String>(
|
||||
value: '白酒',
|
||||
items: ['白酒', '葡萄酒', '洋酒', '啤酒', '黄酒']
|
||||
.map((s) => DropdownMenuItem(
|
||||
value: s,
|
||||
child: Text(s,
|
||||
style: const TextStyle(fontSize: 13))))
|
||||
.toList(),
|
||||
onChanged: (_) {},
|
||||
decoration: const InputDecoration(),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _Field(
|
||||
label: '品牌',
|
||||
child: TextFormField(
|
||||
decoration: const InputDecoration(hintText: '品牌名称')),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: _Field(
|
||||
label: '规格',
|
||||
child: TextFormField(
|
||||
decoration:
|
||||
const InputDecoration(hintText: '如:500ml/瓶')),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: _Field(
|
||||
label: '单位',
|
||||
child: DropdownButtonFormField<String>(
|
||||
value: '瓶',
|
||||
items: ['瓶', '箱', '件', '桶', '支']
|
||||
.map((s) => DropdownMenuItem(
|
||||
value: s,
|
||||
child: Text(s,
|
||||
style: const TextStyle(fontSize: 13))))
|
||||
.toList(),
|
||||
onChanged: (_) {},
|
||||
decoration: const InputDecoration(),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _Field(
|
||||
label: '成本价',
|
||||
child: TextFormField(
|
||||
decoration: const InputDecoration(
|
||||
hintText: '0.00', prefixText: '¥'),
|
||||
keyboardType:
|
||||
const TextInputType.numberWithOptions(decimal: true),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: _Field(
|
||||
label: '销售价',
|
||||
child: TextFormField(
|
||||
decoration: const InputDecoration(
|
||||
hintText: '0.00', prefixText: '¥'),
|
||||
keyboardType:
|
||||
const TextInputType.numberWithOptions(decimal: true),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: _Field(
|
||||
label: '安全库存',
|
||||
child: TextFormField(
|
||||
decoration: const InputDecoration(hintText: '最低库存量'),
|
||||
keyboardType: TextInputType.number,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_Field(
|
||||
label: '条形码',
|
||||
fullWidth: true,
|
||||
child: TextFormField(
|
||||
decoration: const InputDecoration(hintText: '商品条形码(选填)')),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _Field extends StatelessWidget {
|
||||
final String label;
|
||||
final Widget child;
|
||||
final bool required;
|
||||
final bool fullWidth;
|
||||
|
||||
const _Field({
|
||||
required this.label,
|
||||
required this.child,
|
||||
this.required = false,
|
||||
this.fullWidth = false,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget content = Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(children: [
|
||||
if (required)
|
||||
const Text('* ',
|
||||
style: TextStyle(color: AppTheme.danger, fontSize: 13)),
|
||||
Text(label,
|
||||
style: const TextStyle(
|
||||
fontSize: 13, color: AppTheme.textSecondary)),
|
||||
]),
|
||||
const SizedBox(height: 6),
|
||||
child,
|
||||
],
|
||||
);
|
||||
return fullWidth
|
||||
? SizedBox(width: double.infinity, child: content)
|
||||
: content;
|
||||
}
|
||||
}
|
||||
|
||||
class _StatusBadge extends StatelessWidget {
|
||||
final String status;
|
||||
const _StatusBadge(this.status);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final bool enabled = status == '启用';
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: enabled
|
||||
? const Color(0xFFE8F5E9)
|
||||
: const Color(0xFFF5F5F5),
|
||||
borderRadius: BorderRadius.circular(3),
|
||||
),
|
||||
child: Text(
|
||||
status,
|
||||
style: TextStyle(
|
||||
color: enabled ? AppTheme.success : AppTheme.textSecondary,
|
||||
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),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user