feat: 完成7项功能任务
后端: - fix(backend): InventoryLog 增加 Product/Warehouse 关联,Logs 接口加 Preload 修复流水记录显示"-" - feat(backend): 新增 /inventory/batches 批次追踪接口 - feat(backend): 新增财务记录、编号规则接口(finance、number_rule handler) - fix(backend): service/stock_test.go 修复 model.Date 类型错误 前端: - feat(client): 入库/出库管理 Tab 调换顺序(审核在前),审核 Tab 加新建按钮,列表 Tab 无按钮 - feat(client): 入库单/出库单增加详情弹窗,显示完整单据信息和商品明细 - feat(client): 完善出库单新建表单(StockOutFormScreen),支持选客户/仓库/商品 - fix(client): StockInItem/StockOutItem.fromJson 修复读取嵌套 product 对象而非平铺字段 - feat(client): 批次追踪页面(BatchTrackingScreen)及侧边栏入口 - feat(client): 财务管理、盘点、编号规则接入真实后端 API - fix(client): 入库/出库审核通过后 invalidate inventoryListProvider 刷新库存 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,7 @@ import 'package:go_router/go_router.dart';
|
||||
import '../../core/theme/app_theme.dart';
|
||||
import '../../models/stock_in.dart';
|
||||
import '../../providers/stock_in_provider.dart';
|
||||
import '../../repositories/stock_in_repository.dart';
|
||||
import '../../widgets/data_table_card.dart';
|
||||
import '../../widgets/page_scaffold.dart';
|
||||
import '../../widgets/status_badge.dart';
|
||||
@@ -45,17 +46,17 @@ class _StockInListScreenState extends ConsumerState<StockInListScreen> {
|
||||
return PageScaffold(
|
||||
title: '入库管理',
|
||||
tabs: const [
|
||||
Tab(text: '入库单'),
|
||||
Tab(text: '入库审核'),
|
||||
Tab(text: '入库单'),
|
||||
],
|
||||
tabViews: [
|
||||
_buildListTab(filterStatus: null),
|
||||
_buildListTab(filterStatus: 'pending'),
|
||||
_buildListTab(filterStatus: 'pending', showNewButton: true),
|
||||
_buildListTab(filterStatus: null, showNewButton: false),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildListTab({String? filterStatus}) {
|
||||
Widget _buildListTab({String? filterStatus, required bool showNewButton}) {
|
||||
final asyncOrders = ref.watch(stockInListProvider);
|
||||
return asyncOrders.when(
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
@@ -75,7 +76,6 @@ class _StockInListScreenState extends ConsumerState<StockInListScreen> {
|
||||
),
|
||||
),
|
||||
data: (result) {
|
||||
// Client-side filter for review tab
|
||||
final orders = filterStatus != null
|
||||
? result.data
|
||||
.where((o) => o.status == filterStatus)
|
||||
@@ -86,6 +86,7 @@ class _StockInListScreenState extends ConsumerState<StockInListScreen> {
|
||||
totalCount: filterStatus != null ? orders.length : result.total,
|
||||
page: result.page,
|
||||
showStatusFilter: filterStatus == null,
|
||||
showNewButton: showNewButton,
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -96,6 +97,7 @@ class _StockInListScreenState extends ConsumerState<StockInListScreen> {
|
||||
required int totalCount,
|
||||
required int page,
|
||||
required bool showStatusFilter,
|
||||
required bool showNewButton,
|
||||
}) {
|
||||
return DataTableCard(
|
||||
totalCount: totalCount,
|
||||
@@ -104,11 +106,12 @@ class _StockInListScreenState extends ConsumerState<StockInListScreen> {
|
||||
ref.read(stockInListProvider.notifier).setPage(p),
|
||||
toolbar: Row(
|
||||
children: [
|
||||
ElevatedButton.icon(
|
||||
onPressed: () => context.go('/stock-in/new'),
|
||||
icon: const Icon(Icons.add, size: 16),
|
||||
label: const Text('新建入库单'),
|
||||
),
|
||||
if (showNewButton)
|
||||
ElevatedButton.icon(
|
||||
onPressed: () => context.go('/stock-in/new'),
|
||||
icon: const Icon(Icons.add, size: 16),
|
||||
label: const Text('新建入库审核单'),
|
||||
),
|
||||
const Spacer(),
|
||||
if (showStatusFilter) ...[
|
||||
_StatusFilterDropdown(
|
||||
@@ -187,6 +190,13 @@ class _StockInListScreenState extends ConsumerState<StockInListScreen> {
|
||||
DataCell(Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () => _showDetail(context, o.id),
|
||||
child: const Text('详情',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppTheme.primary)),
|
||||
),
|
||||
if (o.status == 'draft')
|
||||
TextButton(
|
||||
onPressed: () =>
|
||||
@@ -224,6 +234,16 @@ class _StockInListScreenState extends ConsumerState<StockInListScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _showDetail(BuildContext context, int orderId) async {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (ctx) => _StockInDetailDialog(
|
||||
orderId: orderId,
|
||||
repository: ref.read(stockInRepositoryProvider),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _pickDateRange() async {
|
||||
final range = await showDateRangePicker(
|
||||
context: context,
|
||||
@@ -354,6 +374,218 @@ class _StockInListScreenState extends ConsumerState<StockInListScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
// Detail dialog — fetches full order with items
|
||||
class _StockInDetailDialog extends StatefulWidget {
|
||||
final int orderId;
|
||||
final StockInRepository repository;
|
||||
|
||||
const _StockInDetailDialog({
|
||||
required this.orderId,
|
||||
required this.repository,
|
||||
});
|
||||
|
||||
@override
|
||||
State<_StockInDetailDialog> createState() => _StockInDetailDialogState();
|
||||
}
|
||||
|
||||
class _StockInDetailDialogState extends State<_StockInDetailDialog> {
|
||||
late Future<StockInOrder> _future;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_future = widget.repository.get(widget.orderId);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Dialog(
|
||||
child: Container(
|
||||
width: 720,
|
||||
constraints: const BoxConstraints(maxHeight: 600),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 14),
|
||||
decoration: const BoxDecoration(
|
||||
color: AppTheme.primary,
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(12),
|
||||
topRight: Radius.circular(12),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Text('入库单详情',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.white)),
|
||||
const Spacer(),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close, color: Colors.white),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: FutureBuilder<StockInOrder>(
|
||||
future: _future,
|
||||
builder: (context, snap) {
|
||||
if (snap.connectionState == ConnectionState.waiting) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
if (snap.hasError) {
|
||||
return Center(
|
||||
child: Text('加载失败:${snap.error}',
|
||||
style: const TextStyle(color: AppTheme.danger)));
|
||||
}
|
||||
return _buildContent(snap.data!);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildContent(StockInOrder o) {
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Basic info
|
||||
Wrap(
|
||||
spacing: 32,
|
||||
runSpacing: 12,
|
||||
children: [
|
||||
_InfoField('入库单号', o.orderNo),
|
||||
_InfoField('状态', _statusLabel(o.status)),
|
||||
_InfoField('供应商', o.partnerName ?? '-'),
|
||||
_InfoField('仓库', o.warehouseName ?? '-'),
|
||||
_InfoField('入库日期', o.orderDate?.substring(0, 10) ?? '-'),
|
||||
_InfoField('合计金额',
|
||||
o.totalAmount != null ? '¥${o.totalAmount!.toStringAsFixed(2)}' : '-'),
|
||||
if (o.remark != null && o.remark!.isNotEmpty)
|
||||
_InfoField('备注', o.remark!),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
const Text('商品明细',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppTheme.primaryDark)),
|
||||
const SizedBox(height: 8),
|
||||
if (o.items.isEmpty)
|
||||
const Text('无商品明细',
|
||||
style: TextStyle(color: AppTheme.textSecondary))
|
||||
else
|
||||
Table(
|
||||
border: TableBorder.all(color: AppTheme.border, width: 0.5),
|
||||
columnWidths: const {
|
||||
0: FixedColumnWidth(36),
|
||||
1: FlexColumnWidth(3),
|
||||
2: FlexColumnWidth(1.5),
|
||||
3: FlexColumnWidth(1.5),
|
||||
4: FlexColumnWidth(1.5),
|
||||
5: FlexColumnWidth(1.5),
|
||||
},
|
||||
children: [
|
||||
TableRow(
|
||||
decoration: const BoxDecoration(color: Color(0xFFF0F4FF)),
|
||||
children: ['序号', '商品', '规格', '数量', '单价', '金额']
|
||||
.map((h) => Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8, vertical: 8),
|
||||
child: Text(h,
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppTheme.primaryDark)),
|
||||
))
|
||||
.toList(),
|
||||
),
|
||||
...o.items.asMap().entries.map((e) {
|
||||
final i = e.key;
|
||||
final item = e.value;
|
||||
return TableRow(
|
||||
decoration: BoxDecoration(
|
||||
color: i.isEven ? Colors.white : const Color(0xFFFAFAFA)),
|
||||
children: [
|
||||
_TableCell('${i + 1}'),
|
||||
_TableCell(item.productName ?? '-'),
|
||||
_TableCell(item.productSpec ?? '-'),
|
||||
_TableCell(item.quantity.toStringAsFixed(3)),
|
||||
_TableCell('¥${item.unitPrice.toStringAsFixed(2)}'),
|
||||
_TableCell('¥${item.totalPrice.toStringAsFixed(2)}'),
|
||||
],
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _statusLabel(String status) {
|
||||
switch (status) {
|
||||
case 'draft':
|
||||
return '草稿';
|
||||
case 'pending':
|
||||
return '待审核';
|
||||
case 'approved':
|
||||
return '已审核';
|
||||
case 'rejected':
|
||||
return '已拒绝';
|
||||
default:
|
||||
return status;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _InfoField extends StatelessWidget {
|
||||
final String label;
|
||||
final String value;
|
||||
|
||||
const _InfoField(this.label, this.value);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(label,
|
||||
style: const TextStyle(
|
||||
fontSize: 12, color: AppTheme.textSecondary)),
|
||||
const SizedBox(height: 2),
|
||||
Text(value,
|
||||
style: const TextStyle(
|
||||
fontSize: 14, fontWeight: FontWeight.w500)),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _TableCell extends StatelessWidget {
|
||||
final String text;
|
||||
|
||||
const _TableCell(this.text);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
|
||||
child: Text(text, style: const TextStyle(fontSize: 13)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _StatusFilterDropdown extends StatelessWidget {
|
||||
final String value;
|
||||
final ValueChanged<String?> onChanged;
|
||||
|
||||
Reference in New Issue
Block a user