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,549 @@
|
||||
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 StockInFormScreen extends ConsumerStatefulWidget {
|
||||
const StockInFormScreen({super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<StockInFormScreen> createState() => _StockInFormScreenState();
|
||||
}
|
||||
|
||||
class _StockInFormScreenState extends ConsumerState<StockInFormScreen> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
final _orderNoCtrl = TextEditingController(text: 'RK20260404004');
|
||||
final _remarkCtrl = TextEditingController();
|
||||
String _supplier = '贵州茅台酒股份有限公司';
|
||||
String _warehouse = '主仓库';
|
||||
DateTime _orderDate = DateTime.now();
|
||||
bool _submitting = false;
|
||||
|
||||
final List<Map<String, dynamic>> _items = [
|
||||
{
|
||||
'name': '茅台酒(飞天)53度500ml',
|
||||
'spec': '500ml/瓶',
|
||||
'unit': '瓶',
|
||||
'qty': TextEditingController(text: '10'),
|
||||
'price': TextEditingController(text: '2600.00'),
|
||||
},
|
||||
{
|
||||
'name': '五粮液(普五)52度500ml',
|
||||
'spec': '500ml/瓶',
|
||||
'unit': '瓶',
|
||||
'qty': TextEditingController(text: '20'),
|
||||
'price': TextEditingController(text: '1050.00'),
|
||||
},
|
||||
];
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_orderNoCtrl.dispose();
|
||||
_remarkCtrl.dispose();
|
||||
for (final item in _items) {
|
||||
(item['qty'] as TextEditingController).dispose();
|
||||
(item['price'] as TextEditingController).dispose();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
double get _totalAmount {
|
||||
double total = 0;
|
||||
for (final item in _items) {
|
||||
final qty = double.tryParse(
|
||||
(item['qty'] as TextEditingController).text) ??
|
||||
0;
|
||||
final price = double.tryParse(
|
||||
(item['price'] as TextEditingController).text) ??
|
||||
0;
|
||||
total += qty * price;
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
void _addItem() {
|
||||
setState(() {
|
||||
_items.add({
|
||||
'name': '',
|
||||
'spec': '',
|
||||
'unit': '瓶',
|
||||
'qty': TextEditingController(),
|
||||
'price': TextEditingController(),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void _removeItem(int index) {
|
||||
setState(() {
|
||||
final item = _items.removeAt(index);
|
||||
(item['qty'] as TextEditingController).dispose();
|
||||
(item['price'] as TextEditingController).dispose();
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _submit(bool asDraft) async {
|
||||
if (!asDraft && !_formKey.currentState!.validate()) return;
|
||||
setState(() => _submitting = true);
|
||||
await Future.delayed(const Duration(milliseconds: 600));
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(asDraft ? '已保存为草稿' : '入库单已提交审核'),
|
||||
backgroundColor: AppTheme.success,
|
||||
),
|
||||
);
|
||||
context.go('/stock-in');
|
||||
}
|
||||
if (mounted) setState(() => _submitting = false);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: AppTheme.background,
|
||||
body: Column(
|
||||
children: [
|
||||
// Page 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('/stock-in'),
|
||||
tooltip: '返回',
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
const Text('新建入库单',
|
||||
style: TextStyle(
|
||||
fontSize: 16, fontWeight: FontWeight.w600)),
|
||||
const Spacer(),
|
||||
OutlinedButton(
|
||||
onPressed: _submitting ? null : () => _submit(true),
|
||||
child: const Text('保存草稿'),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
ElevatedButton.icon(
|
||||
onPressed: _submitting ? null : () => _submit(false),
|
||||
icon: _submitting
|
||||
? const SizedBox(
|
||||
width: 14,
|
||||
height: 14,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2, color: Colors.white))
|
||||
: const Icon(Icons.send, size: 16),
|
||||
label: const Text('提交审核'),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
OutlinedButton.icon(
|
||||
onPressed: () => context.go('/stock-in'),
|
||||
icon: const Icon(Icons.cancel_outlined, size: 16),
|
||||
label: const Text('取消'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Divider(height: 1),
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Basic info card
|
||||
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: [
|
||||
_FormField(
|
||||
label: '入库单号',
|
||||
child: TextFormField(
|
||||
controller: _orderNoCtrl,
|
||||
readOnly: true,
|
||||
style: const TextStyle(
|
||||
fontFamily: 'monospace'),
|
||||
decoration: const InputDecoration(
|
||||
suffixIcon: Icon(
|
||||
Icons.autorenew,
|
||||
size: 16),
|
||||
),
|
||||
),
|
||||
),
|
||||
_FormField(
|
||||
label: '供应商',
|
||||
required: true,
|
||||
child: DropdownButtonFormField<String>(
|
||||
value: _supplier,
|
||||
items: [
|
||||
'贵州茅台酒股份有限公司',
|
||||
'四川五粮液股份有限公司',
|
||||
'江苏洋河酒厂股份有限公司',
|
||||
'剑南春(集团)有限责任公司',
|
||||
'泸州老窖股份有限公司',
|
||||
'山西汾酒股份有限公司',
|
||||
]
|
||||
.map((s) => DropdownMenuItem(
|
||||
value: s,
|
||||
child: Text(s,
|
||||
style: const TextStyle(
|
||||
fontSize: 13))))
|
||||
.toList(),
|
||||
onChanged: (v) =>
|
||||
setState(() => _supplier = v!),
|
||||
validator: (v) => v == null || v.isEmpty
|
||||
? '请选择供应商'
|
||||
: null,
|
||||
decoration: const InputDecoration(),
|
||||
),
|
||||
),
|
||||
_FormField(
|
||||
label: '入库仓库',
|
||||
required: true,
|
||||
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(),
|
||||
),
|
||||
),
|
||||
_FormField(
|
||||
label: '入库日期',
|
||||
required: true,
|
||||
child: InkWell(
|
||||
onTap: _pickDate,
|
||||
child: InputDecorator(
|
||||
decoration: const InputDecoration(),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
'${_orderDate.year}-${_orderDate.month.toString().padLeft(2, '0')}-${_orderDate.day.toString().padLeft(2, '0')}',
|
||||
style: const TextStyle(
|
||||
fontSize: 13),
|
||||
),
|
||||
),
|
||||
const Icon(
|
||||
Icons.calendar_today,
|
||||
size: 16,
|
||||
color: AppTheme.textSecondary),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_FormField(
|
||||
label: '备注',
|
||||
width: double.infinity,
|
||||
child: TextFormField(
|
||||
controller: _remarkCtrl,
|
||||
maxLines: 2,
|
||||
decoration: const InputDecoration(
|
||||
hintText: '选填,如有特殊说明请在此注明',
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
// Items card
|
||||
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 Spacer(),
|
||||
ElevatedButton.icon(
|
||||
onPressed: _addItem,
|
||||
icon: const Icon(Icons.add, size: 16),
|
||||
label: const Text('添加商品'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
minimumSize: const Size(0, 32)),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
// Items table
|
||||
Table(
|
||||
columnWidths: const {
|
||||
0: FixedColumnWidth(36),
|
||||
1: FlexColumnWidth(3),
|
||||
2: FlexColumnWidth(2),
|
||||
3: FixedColumnWidth(60),
|
||||
4: FlexColumnWidth(1.5),
|
||||
5: FlexColumnWidth(1.5),
|
||||
6: FlexColumnWidth(1.5),
|
||||
7: FixedColumnWidth(60),
|
||||
},
|
||||
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(
|
||||
_items.length,
|
||||
(i) => _buildItemRow(i)),
|
||||
],
|
||||
),
|
||||
const Divider(height: 1),
|
||||
// Total
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 12),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
const Text('合计金额:',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500)),
|
||||
Text(
|
||||
'¥${_totalAmount.toStringAsFixed(2)}',
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppTheme.danger),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
TableRow _buildItemRow(int index) {
|
||||
final item = _items[index];
|
||||
final qtyCtrl = item['qty'] as TextEditingController;
|
||||
final priceCtrl = item['price'] as TextEditingController;
|
||||
final qty = double.tryParse(qtyCtrl.text) ?? 0;
|
||||
final price = double.tryParse(priceCtrl.text) ?? 0;
|
||||
final amount = qty * price;
|
||||
|
||||
return TableRow(
|
||||
decoration: BoxDecoration(
|
||||
color: index.isEven ? Colors.white : const Color(0xFFFAFAFA),
|
||||
),
|
||||
children: [
|
||||
// Index
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
|
||||
child: Text('${index + 1}',
|
||||
style: const TextStyle(
|
||||
fontSize: 13, color: AppTheme.textSecondary)),
|
||||
),
|
||||
// Name
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(4),
|
||||
child: TextFormField(
|
||||
initialValue: item['name'] as String,
|
||||
decoration:
|
||||
const InputDecoration(hintText: '商品名称'),
|
||||
style: const TextStyle(fontSize: 13),
|
||||
onChanged: (v) => item['name'] = v,
|
||||
validator: (v) =>
|
||||
(v == null || v.isEmpty) ? '必填' : null,
|
||||
),
|
||||
),
|
||||
// Spec
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(4),
|
||||
child: TextFormField(
|
||||
initialValue: item['spec'] as String,
|
||||
decoration: const InputDecoration(hintText: '规格'),
|
||||
style: const TextStyle(fontSize: 13),
|
||||
onChanged: (v) => item['spec'] = v,
|
||||
),
|
||||
),
|
||||
// Unit
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(4),
|
||||
child: DropdownButtonFormField<String>(
|
||||
value: item['unit'] as String,
|
||||
items: ['瓶', '箱', '件', '桶', '支']
|
||||
.map((u) => DropdownMenuItem(
|
||||
value: u,
|
||||
child: Text(u, style: const TextStyle(fontSize: 13))))
|
||||
.toList(),
|
||||
onChanged: (v) => setState(() => item['unit'] = v!),
|
||||
decoration: const InputDecoration(),
|
||||
),
|
||||
),
|
||||
// Qty
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(4),
|
||||
child: TextFormField(
|
||||
controller: qtyCtrl,
|
||||
decoration: const InputDecoration(hintText: '0'),
|
||||
style: const TextStyle(fontSize: 13),
|
||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.allow(RegExp(r'^\d+\.?\d{0,2}'))
|
||||
],
|
||||
onChanged: (_) => setState(() {}),
|
||||
validator: (v) {
|
||||
if (v == null || v.isEmpty) return '必填';
|
||||
if ((double.tryParse(v) ?? 0) <= 0) return '>0';
|
||||
return null;
|
||||
},
|
||||
),
|
||||
),
|
||||
// Price
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(4),
|
||||
child: TextFormField(
|
||||
controller: priceCtrl,
|
||||
decoration: const InputDecoration(
|
||||
hintText: '0.00', prefixText: '¥'),
|
||||
style: const TextStyle(fontSize: 13),
|
||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.allow(RegExp(r'^\d+\.?\d{0,2}'))
|
||||
],
|
||||
onChanged: (_) => setState(() {}),
|
||||
validator: (v) {
|
||||
if (v == null || v.isEmpty) return '必填';
|
||||
if ((double.tryParse(v) ?? 0) <= 0) return '>0';
|
||||
return null;
|
||||
},
|
||||
),
|
||||
),
|
||||
// Amount
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
|
||||
child: Text(
|
||||
'¥${amount.toStringAsFixed(2)}',
|
||||
style: const TextStyle(
|
||||
fontSize: 13, fontWeight: FontWeight.w500),
|
||||
),
|
||||
),
|
||||
// Actions
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 4),
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.delete_outline,
|
||||
size: 18, color: AppTheme.danger),
|
||||
onPressed: _items.length > 1 ? () => _removeItem(index) : null,
|
||||
tooltip: '删除',
|
||||
padding: EdgeInsets.zero,
|
||||
constraints:
|
||||
const BoxConstraints(minWidth: 28, minHeight: 28),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _pickDate() async {
|
||||
final date = await showDatePicker(
|
||||
context: context,
|
||||
initialDate: _orderDate,
|
||||
firstDate: DateTime(2020),
|
||||
lastDate: DateTime(2030),
|
||||
);
|
||||
if (date != null) setState(() => _orderDate = date);
|
||||
}
|
||||
}
|
||||
|
||||
class _FormField extends StatelessWidget {
|
||||
final String label;
|
||||
final Widget child;
|
||||
final bool required;
|
||||
final double width;
|
||||
|
||||
const _FormField({
|
||||
required this.label,
|
||||
required this.child,
|
||||
this.required = false,
|
||||
this.width = 240,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
width: width,
|
||||
child: 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,
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,395 @@
|
||||
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 '../../widgets/status_badge.dart';
|
||||
import '../../core/theme/app_theme.dart';
|
||||
|
||||
class StockInListScreen extends ConsumerStatefulWidget {
|
||||
const StockInListScreen({super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<StockInListScreen> createState() =>
|
||||
_StockInListScreenState();
|
||||
}
|
||||
|
||||
class _StockInListScreenState extends ConsumerState<StockInListScreen> {
|
||||
int _page = 1;
|
||||
final _searchCtrl = TextEditingController();
|
||||
String _statusFilter = '全部';
|
||||
DateTimeRange? _dateRange;
|
||||
|
||||
final List<Map<String, dynamic>> _mockOrders = [
|
||||
{
|
||||
'no': 'RK20260401001',
|
||||
'supplier': '贵州茅台酒股份有限公司',
|
||||
'warehouse': '主仓库',
|
||||
'amount': '85000.00',
|
||||
'status': OrderStatus.approved,
|
||||
'creator': '张三',
|
||||
'date': '2026-04-01',
|
||||
},
|
||||
{
|
||||
'no': 'RK20260401002',
|
||||
'supplier': '四川五粮液股份有限公司',
|
||||
'warehouse': '副仓库',
|
||||
'amount': '42500.00',
|
||||
'status': OrderStatus.pending,
|
||||
'creator': '李四',
|
||||
'date': '2026-04-01',
|
||||
},
|
||||
{
|
||||
'no': 'RK20260402001',
|
||||
'supplier': '江苏洋河酒厂股份有限公司',
|
||||
'warehouse': '主仓库',
|
||||
'amount': '36800.00',
|
||||
'status': OrderStatus.approved,
|
||||
'creator': '王五',
|
||||
'date': '2026-04-02',
|
||||
},
|
||||
{
|
||||
'no': 'RK20260402002',
|
||||
'supplier': '剑南春(集团)有限责任公司',
|
||||
'warehouse': '主仓库',
|
||||
'amount': '28600.00',
|
||||
'status': OrderStatus.draft,
|
||||
'creator': '张三',
|
||||
'date': '2026-04-02',
|
||||
},
|
||||
{
|
||||
'no': 'RK20260403001',
|
||||
'supplier': '泸州老窖股份有限公司',
|
||||
'warehouse': '副仓库',
|
||||
'amount': '55200.00',
|
||||
'status': OrderStatus.approved,
|
||||
'creator': '李四',
|
||||
'date': '2026-04-03',
|
||||
},
|
||||
{
|
||||
'no': 'RK20260403002',
|
||||
'supplier': '古井贡酒股份有限公司',
|
||||
'warehouse': '主仓库',
|
||||
'amount': '19800.00',
|
||||
'status': OrderStatus.rejected,
|
||||
'creator': '王五',
|
||||
'date': '2026-04-03',
|
||||
},
|
||||
{
|
||||
'no': 'RK20260403003',
|
||||
'supplier': '贵州茅台酒股份有限公司',
|
||||
'warehouse': '主仓库',
|
||||
'amount': '126000.00',
|
||||
'status': OrderStatus.pending,
|
||||
'creator': '张三',
|
||||
'date': '2026-04-03',
|
||||
},
|
||||
{
|
||||
'no': 'RK20260404001',
|
||||
'supplier': '山西汾酒股份有限公司',
|
||||
'warehouse': '主仓库',
|
||||
'amount': '33600.00',
|
||||
'status': OrderStatus.approved,
|
||||
'creator': '李四',
|
||||
'date': '2026-04-04',
|
||||
},
|
||||
{
|
||||
'no': 'RK20260404002',
|
||||
'supplier': '郎酒股份有限公司',
|
||||
'warehouse': '副仓库',
|
||||
'amount': '47300.00',
|
||||
'status': OrderStatus.draft,
|
||||
'creator': '王五',
|
||||
'date': '2026-04-04',
|
||||
},
|
||||
{
|
||||
'no': 'RK20260404003',
|
||||
'supplier': '四川五粮液股份有限公司',
|
||||
'warehouse': '主仓库',
|
||||
'amount': '68500.00',
|
||||
'status': OrderStatus.pending,
|
||||
'creator': '张三',
|
||||
'date': '2026-04-04',
|
||||
},
|
||||
];
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_searchCtrl.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
List<Map<String, dynamic>> _filteredOrders({OrderStatus? forceStatus}) {
|
||||
return _mockOrders.where((o) {
|
||||
if (forceStatus != null && o['status'] != forceStatus) return false;
|
||||
if (_statusFilter != '全部' && forceStatus == null) {
|
||||
final map = {
|
||||
'草稿': OrderStatus.draft,
|
||||
'待审核': OrderStatus.pending,
|
||||
'已审核': OrderStatus.approved,
|
||||
'已拒绝': OrderStatus.rejected,
|
||||
};
|
||||
if (o['status'] != map[_statusFilter]) return false;
|
||||
}
|
||||
final q = _searchCtrl.text.toLowerCase();
|
||||
if (q.isNotEmpty) {
|
||||
final no = (o['no'] as String).toLowerCase();
|
||||
final supplier = (o['supplier'] as String).toLowerCase();
|
||||
if (!no.contains(q) && !supplier.contains(q)) return false;
|
||||
}
|
||||
return true;
|
||||
}).toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return PageScaffold(
|
||||
title: '入库管理',
|
||||
tabs: const [
|
||||
Tab(text: '入库单'),
|
||||
Tab(text: '入库查询'),
|
||||
Tab(text: '入库审核'),
|
||||
],
|
||||
tabViews: [
|
||||
_buildOrderList(),
|
||||
_buildQueryView(),
|
||||
_buildOrderList(forceStatus: OrderStatus.pending),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildOrderList({OrderStatus? forceStatus}) {
|
||||
final orders = _filteredOrders(forceStatus: forceStatus);
|
||||
return DataTableCard(
|
||||
totalCount: orders.length,
|
||||
page: _page,
|
||||
onPageChanged: (p) => setState(() => _page = p),
|
||||
toolbar: Row(
|
||||
children: [
|
||||
ElevatedButton.icon(
|
||||
onPressed: () => context.go('/stock-in/new'),
|
||||
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 SizedBox(width: 8),
|
||||
OutlinedButton.icon(
|
||||
onPressed: () {},
|
||||
icon: const Icon(Icons.print_outlined, size: 16),
|
||||
label: const Text('打印'),
|
||||
),
|
||||
const Spacer(),
|
||||
if (forceStatus == null) ...[
|
||||
_StatusFilterDropdown(
|
||||
value: _statusFilter,
|
||||
onChanged: (v) => setState(() {
|
||||
_statusFilter = v!;
|
||||
_page = 1;
|
||||
}),
|
||||
),
|
||||
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),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
OutlinedButton.icon(
|
||||
onPressed: _pickDateRange,
|
||||
icon: const Icon(Icons.date_range, size: 16),
|
||||
label: Text(
|
||||
_dateRange == null
|
||||
? '选择日期'
|
||||
: '${_dateRange!.start.toString().substring(0, 10)} ~ ${_dateRange!.end.toString().substring(0, 10)}',
|
||||
style: const TextStyle(fontSize: 13),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
columns: const [
|
||||
DataColumn(label: Text('入库单号')),
|
||||
DataColumn(label: Text('供应商')),
|
||||
DataColumn(label: Text('仓库')),
|
||||
DataColumn(label: Text('金额'), numeric: true),
|
||||
DataColumn(label: Text('状态')),
|
||||
DataColumn(label: Text('录入人')),
|
||||
DataColumn(label: Text('日期')),
|
||||
DataColumn(label: Text('操作')),
|
||||
],
|
||||
rows: orders
|
||||
.map((o) => DataRow(cells: [
|
||||
DataCell(Text(o['no'] as String,
|
||||
style: const TextStyle(
|
||||
color: AppTheme.primary,
|
||||
fontFamily: 'monospace',
|
||||
fontSize: 12))),
|
||||
DataCell(Text(o['supplier'] as String)),
|
||||
DataCell(Text(o['warehouse'] as String)),
|
||||
DataCell(Text('¥${o['amount']}')),
|
||||
DataCell(StatusBadge(o['status'] as OrderStatus)),
|
||||
DataCell(Text(o['creator'] as String)),
|
||||
DataCell(Text(o['date'] as String)),
|
||||
DataCell(Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () {},
|
||||
child: const Text('查看',
|
||||
style: TextStyle(fontSize: 12))),
|
||||
if ((o['status'] as OrderStatus) ==
|
||||
OrderStatus.pending)
|
||||
TextButton(
|
||||
onPressed: () {},
|
||||
child: const Text('审核',
|
||||
style: TextStyle(
|
||||
color: AppTheme.success,
|
||||
fontSize: 12))),
|
||||
if ((o['status'] as OrderStatus) ==
|
||||
OrderStatus.draft)
|
||||
TextButton(
|
||||
onPressed: () {},
|
||||
child: const Text('编辑',
|
||||
style: TextStyle(fontSize: 12))),
|
||||
if ((o['status'] as OrderStatus) ==
|
||||
OrderStatus.draft)
|
||||
TextButton(
|
||||
onPressed: () {},
|
||||
child: const Text('删除',
|
||||
style: TextStyle(
|
||||
color: AppTheme.danger,
|
||||
fontSize: 12))),
|
||||
],
|
||||
)),
|
||||
]))
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildQueryView() {
|
||||
return Column(
|
||||
children: [
|
||||
Container(
|
||||
color: AppTheme.surface,
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Row(
|
||||
children: [
|
||||
const Text('查询条件:', style: TextStyle(fontSize: 13)),
|
||||
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(() => _page = 1),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
_StatusFilterDropdown(
|
||||
value: _statusFilter,
|
||||
onChanged: (v) => setState(() {
|
||||
_statusFilter = v!;
|
||||
_page = 1;
|
||||
}),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
OutlinedButton.icon(
|
||||
onPressed: _pickDateRange,
|
||||
icon: const Icon(Icons.date_range, size: 16),
|
||||
label: Text(
|
||||
_dateRange == null ? '选择日期范围' : '${_dateRange!.start.toString().substring(0, 10)} ~ ${_dateRange!.end.toString().substring(0, 10)}',
|
||||
style: const TextStyle(fontSize: 13),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
ElevatedButton.icon(
|
||||
onPressed: () => setState(() {}),
|
||||
icon: const Icon(Icons.search, size: 16),
|
||||
label: const Text('查询'),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
OutlinedButton(
|
||||
onPressed: () => setState(() {
|
||||
_searchCtrl.clear();
|
||||
_statusFilter = '全部';
|
||||
_dateRange = null;
|
||||
}),
|
||||
child: const Text('重置'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Divider(height: 1),
|
||||
Expanded(child: _buildOrderList()),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _pickDateRange() async {
|
||||
final range = await showDateRangePicker(
|
||||
context: context,
|
||||
firstDate: DateTime(2020),
|
||||
lastDate: DateTime(2030),
|
||||
initialDateRange: _dateRange,
|
||||
);
|
||||
if (range != null) setState(() => _dateRange = range);
|
||||
}
|
||||
}
|
||||
|
||||
class _StatusFilterDropdown extends StatelessWidget {
|
||||
final String value;
|
||||
final ValueChanged<String?> onChanged;
|
||||
|
||||
const _StatusFilterDropdown({
|
||||
required this.value,
|
||||
required this.onChanged,
|
||||
});
|
||||
|
||||
@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: ['全部', '草稿', '待审核', '已审核', '已拒绝']
|
||||
.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