feat: 财务结清、酒行信息、库存备注编辑、标签溯源、入库必填校验

后端
- 新增 shop handler:GET/PUT /shop/info(管理员权限)
- 新增 finance CloseByRef:按单据 ref_type+ref_id 结清账款
- 新增 inventory UpdateRemark:PUT /inventory/:id/remark
- 入库/出库审批自动生成财务应付/应收记录(去除金额>0限制)
- 种子数据 S001-S003 补充真实门店信息

前端
- 设置页新增「酒行信息」Tab,管理员可编辑门店名称/地址/电话/负责人
- 入库单列表新增结清按钮(含确认弹窗),出库单同步
- 入库表单:规格、系列、生产日期、供应商、商品名称改为提交必填
- 入库/出库列表新增入库时间、出库时间、创建时间列
- 商品标签标题改为读取 shop 表门店名,扫码文案改为「扫码溯源 · TRACE」
- 标签页脚显示门店地址和电话(从 API 读取,不再依赖编译时 dart-define)
- 库存备注支持点击编辑,超4字截断显示+Hover展示全文
- ApiClient 新增 patch() 方法(已改用 PUT 规避 CORS)

文档
- 新增 docs/user-manual.md 完整用户操作手册(12章)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-05-23 14:05:41 +08:00
parent c77e1c1490
commit c1ed81dfab
60 changed files with 4664 additions and 1572 deletions
@@ -72,6 +72,13 @@ class _StockInFormScreenState extends ConsumerState<StockInFormScreen> {
row.selectedNameId = nameOpts.where((o) => o.name == item.productName).firstOrNull?.id;
row.selectedSeriesId = seriesOpts.where((o) => o.name == item.productSeries).firstOrNull?.id;
row.selectedSpecId = specOpts.where((o) => o.name == item.productSpec).firstOrNull?.id;
if (item.batchNo != null) row.batchNoCtrl.text = item.batchNo!;
if (item.productionDate != null) {
row.productionDateCtrl.text = item.productionDate!.length >= 10
? item.productionDate!.substring(0, 10)
: item.productionDate!;
row.productionDate = DateTime.tryParse(item.productionDate!);
}
_items.add(row);
}
if (_items.isEmpty) _items.add(_ItemRow());
@@ -112,7 +119,7 @@ class _StockInFormScreenState extends ConsumerState<StockInFormScreen> {
.read(inventoryRepositoryProvider)
.listInventory(warehouseId: warehouseId, pageSize: 500);
setState(() {
_inventoryMap = {for (final inv in result.data) inv.productId: inv.quantity};
_inventoryMap = {for (final inv in result.data.where((inv) => inv.productId != null)) inv.productId!: inv.quantity};
});
} catch (_) {}
}
@@ -147,6 +154,53 @@ class _StockInFormScreenState extends ConsumerState<StockInFormScreen> {
);
return;
}
final invalidQtyIndex = _items.indexWhere(
(item) => (double.tryParse(item.qtyCtrl.text) ?? 0) <= 0);
if (invalidQtyIndex >= 0) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('${invalidQtyIndex + 1} 行数量必须大于 0'),
backgroundColor: AppTheme.danger,
),
);
return;
}
if (!asDraft) {
if (_partnerId == null) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('请选择供应商'), backgroundColor: AppTheme.danger),
);
return;
}
for (int i = 0; i < _items.length; i++) {
final item = _items[i];
if (item.selectedNameId == null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('${i + 1} 行请选择商品名称'), backgroundColor: AppTheme.danger),
);
return;
}
if (item.selectedSeriesId == null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('${i + 1} 行请选择系列'), backgroundColor: AppTheme.danger),
);
return;
}
if (item.selectedSpecId == null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('${i + 1} 行请选择规格'), backgroundColor: AppTheme.danger),
);
return;
}
if (item.productionDateCtrl.text.trim().isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('${i + 1} 行请填写生产日期'), backgroundColor: AppTheme.danger),
);
return;
}
}
}
setState(() => _submitting = true);
@@ -187,11 +241,15 @@ class _StockInFormScreenState extends ConsumerState<StockInFormScreen> {
final itemsData = _items.map((item) {
final qty = double.tryParse(item.qtyCtrl.text) ?? 0;
final price = double.tryParse(item.priceCtrl.text) ?? 0;
final batchNo = item.batchNoCtrl.text.trim();
final productionDate = item.productionDateCtrl.text.trim();
return {
'product_id': item.productId ?? 0,
'quantity': qty,
'unit_price': price,
'total_price': qty * price,
if (batchNo.isNotEmpty) 'batch_no': batchNo,
if (productionDate.isNotEmpty) 'production_date': productionDate,
};
}).toList();
@@ -344,6 +402,7 @@ class _StockInFormScreenState extends ConsumerState<StockInFormScreen> {
),
_FormField(
label: '供应商',
required: true,
child: asyncSuppliers.when(
loading: () => const LinearProgressIndicator(),
error: (_, __) => const Text('加载失败'),
@@ -358,6 +417,7 @@ class _StockInFormScreenState extends ConsumerState<StockInFormScreen> {
style: const TextStyle(fontSize: 13))))
.toList(),
onChanged: (v) => setState(() => _partnerId = v),
validator: (v) => v == null ? '不能为空' : null,
decoration: const InputDecoration(),
),
),
@@ -439,22 +499,24 @@ class _StockInFormScreenState extends ConsumerState<StockInFormScreen> {
const SizedBox(height: 12),
Table(
columnWidths: const {
0: FixedColumnWidth(36),
1: FlexColumnWidth(2.2),
2: FlexColumnWidth(1.3),
3: FlexColumnWidth(1.3),
4: FlexColumnWidth(0.9),
5: FlexColumnWidth(1.0),
6: FlexColumnWidth(1.0),
7: FlexColumnWidth(1.0),
8: FlexColumnWidth(1.0),
9: FixedColumnWidth(60),
0: FixedColumnWidth(36), // 序号
1: FlexColumnWidth(1.2), // 商品编码
2: FlexColumnWidth(2.0), // 名称
3: FlexColumnWidth(1.3), // 系列
4: FlexColumnWidth(1.3), // 规格
5: FlexColumnWidth(0.9), // 单品数量
6: FlexColumnWidth(1.0), // 数量
7: FlexColumnWidth(1.0), // 单价
8: FlexColumnWidth(1.0), // 金额
9: FlexColumnWidth(1.2), // 批次号
10: FlexColumnWidth(1.2), // 生产日期
11: FixedColumnWidth(60), // 操作
},
children: [
TableRow(
decoration: const BoxDecoration(color: Color(0xFFF0F4FF)),
children: [
'序号', '名称', '系列', '规格', '单品数量', '数量', '单价', '金额', '当前库存', '操作',
'序号', '商品编码', '名称', '系列', '规格', '单品数量', '数量', '单价', '金额', '批次号', '生产日期', '操作',
]
.map((h) => Padding(
padding: const EdgeInsets.symmetric(
@@ -515,6 +577,10 @@ class _StockInFormScreenState extends ConsumerState<StockInFormScreen> {
?.where((o) => o.id == item.selectedSpecId)
.firstOrNull
?.quantity ?? 0;
final productCode = asyncNames.valueOrNull
?.where((o) => o.id == item.selectedNameId)
.firstOrNull
?.code ?? '';
return TableRow(
decoration: BoxDecoration(
@@ -527,6 +593,12 @@ class _StockInFormScreenState extends ConsumerState<StockInFormScreen> {
child: Text('${index + 1}',
style: const TextStyle(fontSize: 13, color: AppTheme.textSecondary)),
),
// 商品编码
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 12),
child: Text(productCode,
style: const TextStyle(fontSize: 12, color: AppTheme.textSecondary)),
),
// 名称
Padding(
padding: const EdgeInsets.all(4),
@@ -561,6 +633,7 @@ class _StockInFormScreenState extends ConsumerState<StockInFormScreen> {
selectedId: item.selectedSeriesId,
hint: '选择系列',
dialogTitle: '选择系列',
isRequired: true,
onChanged: (v) => setState(() {
item.selectedSeriesId = v;
item.productId = null;
@@ -647,8 +720,51 @@ class _StockInFormScreenState extends ConsumerState<StockInFormScreen> {
style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w500),
),
),
// 当前库存
_buildInventoryCell(item.productId),
// 批次号
Padding(
padding: const EdgeInsets.all(4),
child: TextFormField(
controller: item.batchNoCtrl,
decoration: const InputDecoration(
hintText: '选填',
labelText: '批次号',
isDense: true,
),
style: const TextStyle(fontSize: 13),
onChanged: (_) => setState(() {}),
),
),
// 生产日期
Padding(
padding: const EdgeInsets.all(4),
child: TextFormField(
controller: item.productionDateCtrl,
readOnly: true,
decoration: const InputDecoration(
hintText: '请选择',
labelText: '* 生产日期',
isDense: true,
suffixIcon: Icon(Icons.calendar_today, size: 14),
),
style: const TextStyle(fontSize: 13),
onTap: () async {
final date = await showDatePicker(
context: context,
initialDate: item.productionDate ?? DateTime.now(),
firstDate: DateTime(2000),
lastDate: DateTime(2100),
locale: const Locale('zh', 'CN'),
);
if (date != null) {
setState(() {
item.productionDate = date;
item.productionDateCtrl.text =
'${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}';
});
}
},
),
),
// 操作
Padding(
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 4),
@@ -693,6 +809,7 @@ class _StockInFormScreenState extends ConsumerState<StockInFormScreen> {
initialDate: _orderDate,
firstDate: DateTime(2020),
lastDate: DateTime(2030),
locale: const Locale('zh', 'CN'),
);
if (date != null) setState(() => _orderDate = date);
}
@@ -703,12 +820,17 @@ class _ItemRow {
int? selectedNameId;
int? selectedSeriesId;
int? selectedSpecId;
final TextEditingController qtyCtrl = TextEditingController();
final TextEditingController qtyCtrl = TextEditingController(text: '1');
final TextEditingController priceCtrl = TextEditingController();
final TextEditingController batchNoCtrl = TextEditingController();
final TextEditingController productionDateCtrl = TextEditingController();
DateTime? productionDate;
void dispose() {
qtyCtrl.dispose();
priceCtrl.dispose();
batchNoCtrl.dispose();
productionDateCtrl.dispose();
}
}
@@ -1,3 +1,4 @@
import '../../core/utils/dialog_util.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
@@ -10,7 +11,13 @@ import '../../widgets/multi_select_dropdown.dart' show ColDef, ColumnToggleButto
import '../../widgets/page_scaffold.dart';
import '../../widgets/status_badge.dart';
import '../../core/utils/export_util.dart';
import '../../core/utils/print_util.dart';
import '../../providers/inventory_provider.dart';
import '../../providers/tab_state_provider.dart';
import '../../providers/product_provider.dart' show productRepositoryProvider;
import '../../repositories/product_repository.dart';
import '../../providers/finance_provider.dart' show financeRepositoryProvider;
import '../../providers/shop_provider.dart' show shopInfoProvider;
class StockInListScreen extends ConsumerStatefulWidget {
const StockInListScreen({super.key});
@@ -33,6 +40,7 @@ class _StockInListScreenState extends ConsumerState<StockInListScreen> {
ColDef('amount', '金额', minWidth: 800),
ColDef('status', '状态'),
ColDef('date', '日期', minWidth: 900),
ColDef('reviewed_at', '入库时间', minWidth: 900),
ColDef('operator', '入库员', minWidth: 1100),
ColDef('reviewer', '审核员', minWidth: 1100),
ColDef('actions', '操作', required: true),
@@ -63,13 +71,15 @@ class _StockInListScreenState extends ConsumerState<StockInListScreen> {
Widget build(BuildContext context) {
return PageScaffold(
title: '入库管理',
initialTab: ref.read(stockInTabProvider),
onTabChanged: (i) => ref.read(stockInTabProvider.notifier).state = i,
tabs: const [
Tab(text: '入库审核'),
Tab(text: '入库单'),
Tab(text: '入库审核'),
],
tabViews: [
_buildListTab(filterStatus: 'pending', showNewButton: true),
_buildListTab(filterStatus: 'exclude_pending', showNewButton: false),
_buildListTab(filterStatus: 'pending', showNewButton: true),
],
);
}
@@ -211,6 +221,12 @@ class _StockInListScreenState extends ConsumerState<StockInListScreen> {
return DataCell(StatusBadge(_apiStatusToEnum(o.status)));
case 'date':
return DataCell(Text(o.orderDate?.substring(0, 10) ?? '-'));
case 'reviewed_at':
return DataCell(Text(o.reviewedAt != null
? o.reviewedAt!.length >= 16
? o.reviewedAt!.substring(0, 16)
: o.reviewedAt!.substring(0, 10)
: '-'));
case 'operator':
return DataCell(Text(o.operatorName ?? '-',
style: const TextStyle(fontSize: 13)));
@@ -218,7 +234,9 @@ class _StockInListScreenState extends ConsumerState<StockInListScreen> {
return DataCell(Text(o.reviewerName ?? '-',
style: const TextStyle(fontSize: 13)));
case 'actions':
return DataCell(Row(
return DataCell(SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
TextButton(
@@ -227,6 +245,40 @@ class _StockInListScreenState extends ConsumerState<StockInListScreen> {
style:
TextStyle(fontSize: 12, color: AppTheme.primary)),
),
TextButton(
onPressed: () async {
final order = await ref.read(stockInRepositoryProvider).get(o.id);
await printStockInOrder(order);
},
child: const Text('打印',
style: TextStyle(fontSize: 12, color: AppTheme.primary)),
),
TextButton(
onPressed: () async {
final order = await ref.read(stockInRepositoryProvider).get(o.id);
if (!context.mounted) return;
final shopInfo = ref.read(shopInfoProvider).valueOrNull;
await showDialog(
context: context,
builder: (_) => _LabelPrintDialog(
order: order,
productRepo: ref.read(productRepositoryProvider),
shopName: shopInfo?.name ?? '',
shopAddress: shopInfo?.address ?? '',
shopPhone: shopInfo?.phone ?? '',
),
);
},
child: const Text('打标签',
style: TextStyle(fontSize: 12, color: AppTheme.primary)),
),
if (o.status == 'approved') ...[
TextButton(
onPressed: () => _confirmSettle(context, o.id, 'stock_in'),
child: const Text('结清',
style: TextStyle(fontSize: 12, color: AppTheme.accent)),
),
],
if (o.status == 'draft') ...[
TextButton(
onPressed: () => context.go('/stock-in/edit/${o.id}'),
@@ -264,7 +316,7 @@ class _StockInListScreenState extends ConsumerState<StockInListScreen> {
),
],
],
));
)));
default:
return const DataCell(SizedBox());
}
@@ -362,7 +414,7 @@ class _StockInListScreenState extends ConsumerState<StockInListScreen> {
}
Future<void> _showDetail(BuildContext context, int orderId) async {
showDialog(
showAppDialog(
context: context,
builder: (ctx) => _StockInDetailDialog(
orderId: orderId,
@@ -386,6 +438,38 @@ class _StockInListScreenState extends ConsumerState<StockInListScreen> {
}
}
Future<void> _confirmSettle(BuildContext context, int orderId, String refType) async {
final confirmed = await showDialog<bool>(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('结清确认'),
content: const Text('确认将该单据的账款标记为已结清?'),
actions: [
TextButton(onPressed: () => Navigator.pop(ctx, false), child: const Text('取消')),
TextButton(
onPressed: () => Navigator.pop(ctx, true),
child: const Text('确认结清', style: TextStyle(color: AppTheme.accent)),
),
],
),
);
if (confirmed != true || !context.mounted) return;
try {
await ref.read(financeRepositoryProvider).closeByRef(refType, orderId);
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('已结清'), backgroundColor: AppTheme.success),
);
}
} catch (e) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(e.toString()), backgroundColor: AppTheme.danger),
);
}
}
}
Future<void> _confirmDelete(BuildContext context, StockInOrder o) async {
final confirmed = await showDialog<bool>(
context: context,
@@ -553,11 +637,13 @@ class _StockInDetailDialog extends ConsumerStatefulWidget {
class _StockInDetailDialogState extends ConsumerState<_StockInDetailDialog> {
late Future<StockInOrder> _future;
Map<int, double> _inventoryMap = {};
StockInOrder? _loadedOrder;
@override
void initState() {
super.initState();
_future = widget.repository.get(widget.orderId).then((order) async {
if (mounted) setState(() => _loadedOrder = order);
try {
final result = await ref
.read(inventoryRepositoryProvider)
@@ -565,7 +651,7 @@ class _StockInDetailDialogState extends ConsumerState<_StockInDetailDialog> {
if (mounted) {
setState(() {
_inventoryMap = {
for (final inv in result.data) inv.productId: inv.quantity
for (final inv in result.data.where((inv) => inv.productId != null)) inv.productId!: inv.quantity
};
});
}
@@ -600,6 +686,12 @@ class _StockInDetailDialogState extends ConsumerState<_StockInDetailDialog> {
fontWeight: FontWeight.w600,
color: Colors.white)),
const Spacer(),
if (_loadedOrder != null)
IconButton(
icon: const Icon(Icons.print_outlined, color: Colors.white),
tooltip: '打印',
onPressed: () => printStockInOrder(_loadedOrder!),
),
IconButton(
icon: const Icon(Icons.close, color: Colors.white),
onPressed: () => Navigator.of(context).pop(),
@@ -674,71 +766,38 @@ class _StockInDetailDialogState extends ConsumerState<_StockInDetailDialog> {
border: TableBorder.all(color: AppTheme.border, width: 0.5),
columnWidths: const {
0: FixedColumnWidth(36),
1: FlexColumnWidth(2.5),
2: FlexColumnWidth(1.5),
1: FlexColumnWidth(1.2),
2: FlexColumnWidth(2.2),
3: FlexColumnWidth(1.5),
4: FlexColumnWidth(1.5),
5: FlexColumnWidth(1.5),
6: FlexColumnWidth(1.5),
5: FlexColumnWidth(1.2),
6: FlexColumnWidth(1.2),
7: FlexColumnWidth(1.2),
},
children: [
TableRow(
decoration: const BoxDecoration(color: Color(0xFFF0F4FF)),
children: ['序号', '名称', '系列', '规格', '数量', '单价', '金额', '当前库存']
.asMap().entries.map((e) {
final i = e.key; final h = e.value;
if (i == 0) {
return Padding(
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)),
);
}
return TableCell(
verticalAlignment: TableCellVerticalAlignment.fill,
child: Container(
color: h == '当前库存' ? const Color(0xFFCFE2FF) : Colors.transparent,
child: Center(
child: Text(h, style: TextStyle(
fontSize: 13, fontWeight: FontWeight.w600,
color: h == '当前库存' ? AppTheme.primary : AppTheme.primaryDark)),
),
),
);
}).toList(),
)).toList(),
),
...o.items.asMap().entries.map((e) {
final i = e.key;
final item = e.value;
final qty = item.productId != null ? _inventoryMap[item.productId] : null;
final invColor = qty == null
? AppTheme.textSecondary
: qty <= 0
? AppTheme.danger
: AppTheme.primary;
return TableRow(
decoration: BoxDecoration(
color: i.isEven ? Colors.white : const Color(0xFFFAFAFA)),
children: [
_TableCell('${i + 1}'),
_TableCell(item.productCode ?? '-'),
_TableCell(item.productName ?? '-'),
_TableCell(item.productSeries ?? '-'),
_TableCell(item.productSpec ?? '-'),
_TableCell(item.quantity.toStringAsFixed(3)),
_TableCell('¥${item.unitPrice.toStringAsFixed(2)}'),
_TableCell('¥${item.totalPrice.toStringAsFixed(2)}'),
TableCell(
verticalAlignment: TableCellVerticalAlignment.fill,
child: Container(
color: const Color(0xFFEBF3FF),
child: Center(
child: Text(
qty != null ? qty.toStringAsFixed(0) : '-',
style: TextStyle(fontSize: 13, fontWeight: FontWeight.w700, color: invColor),
),
),
),
),
],
);
}),
@@ -837,3 +896,149 @@ class _StatusFilterDropdown extends StatelessWidget {
);
}
}
class _LabelPrintDialog extends StatefulWidget {
final StockInOrder order;
final ProductRepository productRepo;
final String shopName;
final String shopAddress;
final String shopPhone;
const _LabelPrintDialog({
required this.order,
required this.productRepo,
this.shopName = '',
this.shopAddress = '',
this.shopPhone = '',
});
@override
State<_LabelPrintDialog> createState() => _LabelPrintDialogState();
}
class _LabelPrintDialogState extends State<_LabelPrintDialog> {
late final List<bool> _selected;
bool _printing = false;
String _status = '';
@override
void initState() {
super.initState();
_selected = List.filled(widget.order.items.length, true);
}
Future<void> _print() async {
setState(() { _printing = true; _status = '正在打印...'; });
int done = 0;
for (int i = 0; i < widget.order.items.length; i++) {
if (!_selected[i]) continue;
final item = widget.order.items[i];
try {
final qrBytes = await widget.productRepo.getQRCodeBytes(item.productId);
await printProductLabel(
qrBytes: qrBytes,
name: item.productName ?? '',
code: item.productCode ?? '',
series: item.productSeries,
spec: item.productSpec,
batchNo: item.batchNo,
productionDate: item.productionDate,
shopName: widget.shopName,
shopAddress: widget.shopAddress,
shopPhone: widget.shopPhone,
);
done++;
if (mounted) setState(() => _status = '已打印 $done 张...');
} catch (e) {
if (mounted) setState(() => _status = '${i + 1}行打印失败:$e');
}
}
if (mounted) setState(() { _printing = false; _status = '完成,共打印 $done'; });
}
@override
Widget build(BuildContext context) {
final items = widget.order.items;
return Dialog(
child: Container(
width: 520,
constraints: const BoxConstraints(maxHeight: 520),
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: ListView.separated(
padding: const EdgeInsets.symmetric(vertical: 8),
itemCount: items.length,
separatorBuilder: (_, __) => const Divider(height: 1),
itemBuilder: (_, i) {
final item = items[i];
return CheckboxListTile(
value: _selected[i],
onChanged: _printing
? null
: (v) => setState(() => _selected[i] = v ?? false),
title: Text(
'${item.productCode ?? ''} ${item.productName ?? ''}',
style: const TextStyle(fontSize: 13),
),
subtitle: Text(
'${item.productSeries ?? ''} ${item.productSpec ?? ''}',
style: const TextStyle(fontSize: 12, color: AppTheme.textSecondary),
),
dense: true,
controlAffinity: ListTileControlAffinity.leading,
);
},
),
),
if (_status.isNotEmpty)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
child: Text(_status,
style: const TextStyle(fontSize: 12, color: AppTheme.textSecondary)),
),
Padding(
padding: const EdgeInsets.all(16),
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('关闭'),
),
const SizedBox(width: 8),
ElevatedButton.icon(
onPressed: (_printing || !_selected.contains(true)) ? null : _print,
icon: const Icon(Icons.print_outlined, size: 16),
label: Text(_printing ? '打印中...' : '打印选中'),
),
],
),
),
],
),
),
);
}
}