chore: release v1.0.18
Deploy / build-linux-web (push) Successful in 53s
Deploy / build-windows (push) Successful in 1m48s
Deploy / build-macos (push) Successful in 1m17s
Deploy / build-android (push) Successful in 4m13s
Deploy / build-ios (push) Successful in 9s
Deploy / release-deploy (push) Successful in 1m37s

移动端响应式适配(抽屉导航/列表卡片/弹窗自适应)、Android 正式签名与 APK 发布、
iOS(TestFlight) 工程与 CI、多平台构建流水线、相关文档同步。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-06-07 07:55:33 +08:00
parent 94f0fb2095
commit 480ce836bb
81 changed files with 3096 additions and 366 deletions
+2 -1
View File
@@ -1,5 +1,6 @@
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import '../../core/responsive/responsive.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:intl/intl.dart';
@@ -340,7 +341,7 @@ class _FeedbackDialogState extends ConsumerState<_FeedbackDialog> {
return AlertDialog(
title: Text(isBug ? '反馈 Bug' : '功能建议'),
content: SizedBox(
width: 440,
width: context.dialogWidth(440),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
+3 -2
View File
@@ -1,6 +1,7 @@
import 'dart:async';
import 'package:flutter/material.dart';
import '../../core/responsive/responsive.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:url_launcher/url_launcher.dart';
@@ -125,7 +126,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
borderRadius:
const BorderRadius.vertical(bottom: Radius.circular(4)),
child: SizedBox(
width: 320,
width: context.dialogWidth(320),
child: ListView.separated(
padding: EdgeInsets.zero,
shrinkWrap: true,
@@ -263,7 +264,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8)),
child: Container(
width: 400,
width: context.dialogWidth(400),
padding: const EdgeInsets.all(40),
child: Form(
key: _formKey,
+90 -33
View File
@@ -1,9 +1,11 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../core/responsive/responsive.dart';
import '../../core/theme/app_theme.dart';
import '../../models/finance.dart';
import '../../providers/finance_provider.dart';
import '../../widgets/data_table_card.dart';
import '../../widgets/mobile_list_card.dart';
import '../../widgets/multi_select_dropdown.dart' show ColDef, ColumnToggleButton, FilterableColumnHeader;
import '../../widgets/page_scaffold.dart';
import '../../providers/connectivity_provider.dart';
@@ -129,6 +131,47 @@ class _FinanceTabState extends ConsumerState<_FinanceTab> {
);
}
/// 财务记录:窄屏卡片
Widget _financeCard(FinanceRecord r) {
final canClose = (r.type == 'payable' || r.type == 'receivable') &&
r.status == 'open';
final showStatus = r.type == 'payable' || r.type == 'receivable';
return MobileListCard(
title: Text(r.partnerName?.isNotEmpty == true ? r.partnerName! : r.typeLabel),
subtitle: Text(r.recordDate?.substring(0, 10) ?? '-'),
trailing: _TypeBadge(r.typeLabel),
fields: [
MobileCardField('金额', '¥${r.amount.toStringAsFixed(2)}'),
MobileCardField('余额', null,
valueWidget: Text(
'¥${r.balance.toStringAsFixed(2)}',
style: TextStyle(
fontSize: 13,
color: r.balance > 0
? AppTheme.danger
: AppTheme.textSecondary,
fontWeight: FontWeight.w600,
),
)),
if (r.refType != null && r.refId != null)
MobileCardField(
'关联单据', '${r.refType!.replaceAll('_', '-')}#${r.refId}'),
if (showStatus)
MobileCardField('状态', null, valueWidget: _StatusBadge(r.status)),
if (r.remark?.isNotEmpty == true) MobileCardField('备注', r.remark),
],
actions: canClose
? [
TextButton(
onPressed: () => _closeRecord(r),
child: const Text('结清',
style: TextStyle(fontSize: 13, color: AppTheme.success)),
),
]
: null,
);
}
@override
Widget build(BuildContext context) {
ref.listen(networkRecoveryCountProvider, (_, __) => _refetch());
@@ -302,34 +345,44 @@ class _FinanceTabState extends ConsumerState<_FinanceTab> {
return Column(
children: [
if (widget.typeFilter.isNotEmpty && records.isNotEmpty)
Container(
color: AppTheme.background,
padding: const EdgeInsets.all(12),
child: Row(
children: [
_SummaryCard(
title: '合计金额',
value: '¥${(totalAmount / 10000).toStringAsFixed(2)}',
icon: Icons.account_balance_wallet,
color: AppTheme.primary,
),
const SizedBox(width: 12),
_SummaryCard(
title: '未结清',
value: '¥${(openAmount / 10000).toStringAsFixed(2)}',
icon: Icons.pending_actions,
color: AppTheme.danger,
),
const SizedBox(width: 12),
_SummaryCard(
title: '已结清',
value: '¥${(closedAmount / 10000).toStringAsFixed(2)}',
icon: Icons.check_circle,
color: AppTheme.success,
),
],
),
),
Builder(builder: (context) {
final w = context.isMobile ? 150.0 : null;
final cards = [
_SummaryCard(
title: '合计金额',
value: '¥${(totalAmount / 10000).toStringAsFixed(2)}',
icon: Icons.account_balance_wallet,
color: AppTheme.primary,
width: w,
),
const SizedBox(width: 12),
_SummaryCard(
title: '未结清',
value: '¥${(openAmount / 10000).toStringAsFixed(2)}',
icon: Icons.pending_actions,
color: AppTheme.danger,
width: w,
),
const SizedBox(width: 12),
_SummaryCard(
title: '已结清',
value: '¥${(closedAmount / 10000).toStringAsFixed(2)}',
icon: Icons.check_circle,
color: AppTheme.success,
width: w,
),
];
return Container(
color: AppTheme.background,
padding: const EdgeInsets.all(12),
child: context.isMobile
? SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(children: cards),
)
: Row(children: cards),
);
}),
if (widget.typeFilter.isNotEmpty && records.isNotEmpty)
const Divider(height: 1),
Expanded(
@@ -405,6 +458,7 @@ class _FinanceTabState extends ConsumerState<_FinanceTab> {
),
columns: columns,
rows: rows,
mobileCards: records.map(_financeCard).toList(),
),
),
],
@@ -487,7 +541,7 @@ class _AddPaymentDialogState extends State<_AddPaymentDialog> {
return AlertDialog(
title: Text(_title),
content: SizedBox(
width: 360,
width: context.dialogWidth(360),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
@@ -611,18 +665,19 @@ class _SummaryCard extends StatelessWidget {
final String value;
final IconData icon;
final Color color;
final double? width;
const _SummaryCard({
required this.title,
required this.value,
required this.icon,
required this.color,
this.width,
});
@override
Widget build(BuildContext context) {
return Expanded(
child: Container(
final card = Container(
height: 72,
padding: const EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration(
@@ -659,8 +714,10 @@ class _SummaryCard extends StatelessWidget {
),
],
),
),
);
);
return width != null
? SizedBox(width: width, child: card)
: Expanded(child: card);
}
}
@@ -5,11 +5,13 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../../core/config/app_config.dart';
import '../../core/responsive/responsive.dart';
import '../../core/theme/app_theme.dart';
import '../../models/inventory.dart';
import '../../core/auth/auth_state.dart';
import '../../providers/inventory_provider.dart';
import '../../widgets/data_table_card.dart';
import '../../widgets/mobile_list_card.dart';
import '../../widgets/multi_select_dropdown.dart' show FilterableColumnHeader;
import '../../widgets/page_scaffold.dart';
import '../../core/utils/export_util.dart';
@@ -47,7 +49,7 @@ class _InventoryListScreenState extends ConsumerState<InventoryListScreen> {
builder: (ctx) => AlertDialog(
title: const Text('修改备注'),
content: SizedBox(
width: 360,
width: context.dialogWidth(360),
child: TextField(
controller: ctrl,
autofocus: true,
@@ -188,6 +190,133 @@ class _InventoryListScreenState extends ConsumerState<InventoryListScreen> {
}
}
Future<void> _printLabel(BuildContext context, Inventory item) async {
if (item.productId == null) return;
final qrBytes =
await ref.read(productRepositoryProvider).getQRCodeBytes(item.productId!);
final shopInfo = ref.read(shopInfoProvider).valueOrNull;
if (!context.mounted) return;
await safePrint(
context,
() => printProductLabel(
qrBytes: qrBytes,
name: item.productName,
code: item.productCode,
series: item.series.isEmpty ? null : item.series,
spec: item.spec.isEmpty ? null : item.spec,
batchNo: item.batchNo.isEmpty ? null : item.batchNo,
productionDate: item.productionDate,
remark: item.remark.isEmpty ? null : item.remark,
shopName: shopInfo?.name ?? '',
shopAddress: shopInfo?.address ?? '',
shopPhone: shopInfo?.phone ?? '',
),
);
}
/// 库存查询:窄屏卡片
Widget _inventoryCard(Inventory item) {
return MobileListCard(
onTap: item.productId != null
? () => context.push('/products/${item.productId}')
: null,
title: Text(item.productName.isEmpty ? '-' : item.productName),
subtitle: item.productCode.isEmpty ? null : Text(item.productCode),
trailing: _InventoryStatusBadge(item),
fields: [
if (item.spec.isNotEmpty) MobileCardField('规格', item.spec),
if (item.series.isNotEmpty) MobileCardField('系列', item.series),
if (item.batchNo.isNotEmpty) MobileCardField('批次', item.batchNo),
MobileCardField('仓库', item.warehouseName.isEmpty ? '-' : item.warehouseName),
MobileCardField('库存', null,
valueWidget: Text(
'${item.quantity.toStringAsFixed(0)} ${item.unit}'.trim(),
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: item.quantity == 0
? AppTheme.danger
: (item.minStock != null && item.quantity < item.minStock!)
? AppTheme.accent
: AppTheme.textPrimary,
),
)),
if (item.unitPrice != null)
MobileCardField('单价', '¥${item.unitPrice!.toStringAsFixed(2)}'),
if (item.supplierName.isNotEmpty)
MobileCardField('供应商', item.supplierName),
],
actions: [
TextButton(
onPressed: () => _editRemark(context, item),
child: const Text('备注', style: TextStyle(fontSize: 13)),
),
if (item.productId != null)
TextButton(
onPressed: () => _printLabel(context, item),
child: const Text('打标签', style: TextStyle(fontSize: 13)),
),
],
);
}
/// 库存预警:窄屏卡片
Widget _warningCard(Inventory item) {
return MobileListCard(
onTap: item.productId != null
? () => context.push('/products/${item.productId}')
: null,
title: Text(item.productName.isEmpty ? '-' : item.productName),
subtitle: item.productCode.isEmpty ? null : Text(item.productCode),
trailing: _InventoryStatusBadge(item),
fields: [
MobileCardField('仓库', item.warehouseName.isEmpty ? '-' : item.warehouseName),
MobileCardField('当前库存', null,
valueWidget: Text(
item.quantity.toStringAsFixed(0),
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w700,
color: item.quantity == 0 ? AppTheme.danger : AppTheme.accent),
)),
MobileCardField('安全库存', '${item.minStock}'),
MobileCardField('缺口', null,
valueWidget: Text(
'${item.minStock! - item.quantity.toInt()}',
style: const TextStyle(
fontSize: 13,
color: AppTheme.danger,
fontWeight: FontWeight.w600),
)),
],
);
}
/// 流水记录:窄屏卡片
Widget _logCard(dynamic log) {
return MobileListCard(
title: Text(log.productName ?? '-'),
subtitle: Text(log.createdAt?.substring(0, 19) ?? '-'),
trailing: _DirectionBadge(log.direction),
fields: [
MobileCardField('仓库', log.warehouseName ?? '-'),
MobileCardField('数量', null,
valueWidget: Text(
log.quantity.toStringAsFixed(0),
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: log.direction == 'in'
? AppTheme.success
: AppTheme.danger),
)),
MobileCardField('变前', log.qtyBefore?.toStringAsFixed(0) ?? '-'),
MobileCardField('变后', log.qtyAfter?.toStringAsFixed(0) ?? '-'),
if (log.refType != null) MobileCardField('来源', log.refType),
],
);
}
@override
Widget build(BuildContext context) {
return PageScaffold(
@@ -251,35 +380,45 @@ class _InventoryListScreenState extends ConsumerState<InventoryListScreen> {
return Column(
children: [
// Summary cards
Container(
color: AppTheme.background,
padding: const EdgeInsets.all(12),
child: Row(
children: [
_SummaryCard(
title: '商品总数',
value: '${result.total}',
unit: '',
icon: Icons.inventory_2,
color: AppTheme.primary),
const SizedBox(width: 12),
_SummaryCard(
title: '库存预警',
value: '$warningCount',
unit: '',
icon: Icons.warning_amber,
color: AppTheme.accent),
const SizedBox(width: 12),
_SummaryCard(
title: '缺货商品',
value: '$emptyCount',
unit: '',
icon: Icons.remove_shopping_cart,
color: AppTheme.danger),
],
),
),
// Summary cards(窄屏横向滚动,宽屏等分)
Builder(builder: (context) {
final w = context.isMobile ? 150.0 : null;
final cards = [
_SummaryCard(
title: '商品总数',
value: '${result.total}',
unit: '',
icon: Icons.inventory_2,
color: AppTheme.primary,
width: w),
const SizedBox(width: 12),
_SummaryCard(
title: '库存预警',
value: '$warningCount',
unit: '',
icon: Icons.warning_amber,
color: AppTheme.accent,
width: w),
const SizedBox(width: 12),
_SummaryCard(
title: '缺货商品',
value: '$emptyCount',
unit: '',
icon: Icons.remove_shopping_cart,
color: AppTheme.danger,
width: w),
];
return Container(
color: AppTheme.background,
padding: const EdgeInsets.all(12),
child: context.isMobile
? SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(children: cards),
)
: Row(children: cards),
);
}),
const Divider(height: 1),
Expanded(
child: DataTableCard(
@@ -346,6 +485,8 @@ class _InventoryListScreenState extends ConsumerState<InventoryListScreen> {
),
],
),
mobileCards:
filteredItems.map(_inventoryCard).toList(),
columns: [
const DataColumn(label: Text('商品编码')),
const DataColumn(label: Text('商品名称')),
@@ -580,6 +721,7 @@ class _InventoryListScreenState extends ConsumerState<InventoryListScreen> {
),
],
),
mobileCards: warnings.map(_warningCard).toList(),
columns: const [
DataColumn(label: Text('商品编码')),
DataColumn(label: Text('商品名称')),
@@ -717,6 +859,7 @@ class _InventoryListScreenState extends ConsumerState<InventoryListScreen> {
),
],
),
mobileCards: logs.map(_logCard).toList(),
columns: const [
DataColumn(label: Text('商品名称')),
DataColumn(label: Text('仓库')),
@@ -788,18 +931,21 @@ class _SummaryCard extends StatelessWidget {
final IconData icon;
final Color color;
/// 固定宽度(窄屏横向滚动用);为 null 时占据等分宽度(Expanded,宽屏用)。
final double? width;
const _SummaryCard({
required this.title,
required this.value,
required this.unit,
required this.icon,
required this.color,
this.width,
});
@override
Widget build(BuildContext context) {
return Expanded(
child: Container(
final card = Container(
height: 72,
padding: const EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration(
@@ -849,8 +995,10 @@ class _SummaryCard extends StatelessWidget {
),
],
),
),
);
);
return width != null
? SizedBox(width: width, child: card)
: Expanded(child: card);
}
}
@@ -950,7 +1098,7 @@ class _ImportProgressDialog extends StatelessWidget {
content: ValueListenableBuilder<_ImportState>(
valueListenable: stateNotifier,
builder: (_, state, __) => SizedBox(
width: 320,
width: context.dialogWidth(320),
child: _buildContent(context, state),
),
),
@@ -3,10 +3,12 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../core/models/page_result.dart';
import '../../core/responsive/responsive.dart';
import '../../core/theme/app_theme.dart';
import '../../models/partner.dart';
import '../../providers/partner_provider.dart';
import '../../widgets/data_table_card.dart';
import '../../widgets/mobile_list_card.dart';
import '../../widgets/page_scaffold.dart';
import '../../core/utils/export_util.dart';
@@ -145,12 +147,48 @@ class _PartnersScreenState extends ConsumerState<PartnersScreen> {
required void Function(Partner) onDelete,
}) {
final partners = result.data;
Widget partnerCard(Partner p) {
return MobileListCard(
title: Text(p.name),
subtitle: p.code != null ? Text(p.code!) : null,
trailing: Text(
p.status == 'enabled' ? '正常' : '禁用',
style: TextStyle(
fontSize: 12,
color: p.status == 'enabled'
? AppTheme.success
: AppTheme.textSecondary,
),
),
fields: [
if (p.contact?.isNotEmpty == true) MobileCardField('联系人', p.contact),
if (p.phone?.isNotEmpty == true) MobileCardField('电话', p.phone),
if (p.address?.isNotEmpty == true) MobileCardField('地址', p.address),
],
actions: [
TextButton(
key: Key('btn_edit_${p.id}'),
onPressed: () => onEdit(p),
child: const Text('编辑', style: TextStyle(fontSize: 13)),
),
TextButton(
key: Key('btn_delete_${p.id}'),
onPressed: () => onDelete(p),
child: const Text('删除',
style: TextStyle(fontSize: 13, color: AppTheme.danger)),
),
],
);
}
return DataTableCard(
totalCount: result.total,
page: result.page,
pageSize: result.pageSize,
onPageChanged: onPageChanged,
onPageSizeChanged: onPageSizeChanged,
mobileCards: partners.map(partnerCard).toList(),
toolbar: Row(
children: [
ElevatedButton.icon(
@@ -433,7 +471,7 @@ class _PartnerFormDialogState extends ConsumerState<_PartnerFormDialog> {
final typeName = widget.isSupplier ? '供应商' : '客户';
return Dialog(
child: Container(
width: 520,
width: context.dialogWidth(520),
padding: const EdgeInsets.all(24),
child: Form(
key: _formKey,
@@ -1,9 +1,11 @@
import '../../core/utils/dialog_util.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../core/responsive/responsive.dart';
import '../../core/theme/app_theme.dart';
import '../../providers/product_option_provider.dart';
import '../../widgets/data_table_card.dart';
import '../../widgets/mobile_list_card.dart';
import '../../widgets/page_scaffold.dart';
import '../../core/utils/export_util.dart';
@@ -84,6 +86,23 @@ class _ProductsScreenState extends ConsumerState<ProductsScreen> {
rows: items.map((it) => [it.code ?? '', it.name, it.remark ?? '']).toList(),
),
),
mobileCards: paged
.map((it) => _optionCard(
code: it.code,
name: it.name,
remark: it.remark,
onEdit: () => _showOptionDialog(
title: '编辑商品名称',
hasQuantity: false,
initial: {'code': it.code ?? '', 'name': it.name, 'remark': it.remark ?? ''},
onSave: (data) => ref.read(productNameListProvider.notifier).updateItem(it.id, data),
),
onDelete: () => _confirmDelete(
'删除名称「${it.name}」?',
() => ref.read(productNameListProvider.notifier).delete(it.id),
),
))
.toList(),
columns: const [
DataColumn(label: Text('编号')),
DataColumn(label: Text('名称')),
@@ -156,6 +175,23 @@ class _ProductsScreenState extends ConsumerState<ProductsScreen> {
rows: items.map((it) => [it.code ?? '', it.name, it.remark ?? '']).toList(),
),
),
mobileCards: paged
.map((it) => _optionCard(
code: it.code,
name: it.name,
remark: it.remark,
onEdit: () => _showOptionDialog(
title: '编辑系列',
hasQuantity: false,
initial: {'code': it.code ?? '', 'name': it.name, 'remark': it.remark ?? ''},
onSave: (data) => ref.read(productSeriesListProvider.notifier).updateItem(it.id, data),
),
onDelete: () => _confirmDelete(
'删除系列「${it.name}」?',
() => ref.read(productSeriesListProvider.notifier).delete(it.id),
),
))
.toList(),
columns: const [
DataColumn(label: Text('编号')),
DataColumn(label: Text('系列名称')),
@@ -228,6 +264,24 @@ class _ProductsScreenState extends ConsumerState<ProductsScreen> {
rows: items.map((it) => [it.code ?? '', it.name, it.quantity, it.remark ?? '']).toList(),
),
),
mobileCards: paged
.map((it) => _optionCard(
code: it.code,
name: it.name,
remark: it.remark,
quantity: it.quantity,
onEdit: () => _showOptionDialog(
title: '编辑规格',
hasQuantity: true,
initial: {'code': it.code ?? '', 'name': it.name, 'quantity': it.quantity, 'remark': it.remark ?? ''},
onSave: (data) => ref.read(productSpecListProvider.notifier).updateItem(it.id, data),
),
onDelete: () => _confirmDelete(
'删除规格「${it.name}」?',
() => ref.read(productSpecListProvider.notifier).delete(it.id),
),
))
.toList(),
columns: const [
DataColumn(label: Text('编号')),
DataColumn(label: Text('规格名称')),
@@ -321,6 +375,37 @@ class _ProductsScreenState extends ConsumerState<ProductsScreen> {
);
}
/// 基础数据:窄屏卡片
Widget _optionCard({
required String? code,
required String name,
required String? remark,
int? quantity,
required VoidCallback onEdit,
required VoidCallback onDelete,
}) {
return MobileListCard(
title: Text(name),
subtitle: (code?.isNotEmpty == true) ? Text(code!) : null,
fields: [
if (quantity != null && quantity > 0)
MobileCardField('单品数量', '$quantity'),
if (remark?.isNotEmpty == true) MobileCardField('备注', remark),
],
actions: [
TextButton(
onPressed: onEdit,
child: const Text('编辑', style: TextStyle(fontSize: 13)),
),
TextButton(
onPressed: onDelete,
child: const Text('删除',
style: TextStyle(fontSize: 13, color: AppTheme.danger)),
),
],
);
}
Widget _actionButtons({required VoidCallback onEdit, required VoidCallback onDelete}) {
return Row(
mainAxisSize: MainAxisSize.min,
@@ -386,7 +471,7 @@ class _ProductsScreenState extends ConsumerState<ProductsScreen> {
builder: (ctx) => AlertDialog(
title: Text(title),
content: SizedBox(
width: 360,
width: context.dialogWidth(360),
child: Form(
key: formKey,
child: Column(
@@ -9,6 +9,7 @@ import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../core/auth/auth_state.dart';
import '../../core/config/app_config.dart';
import '../../core/responsive/responsive.dart';
import '../../core/theme/app_theme.dart';
import '../../models/number_rule.dart';
import '../../models/user.dart';
@@ -536,7 +537,7 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
builder: (ctx) => AlertDialog(
title: Text('编辑编号规则 — ${rule.typeLabel}'),
content: SizedBox(
width: 360,
width: context.dialogWidth(360),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
@@ -708,7 +709,7 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
builder: (ctx) => AlertDialog(
title: Text('修改$label'),
content: SizedBox(
width: 320,
width: context.dialogWidth(320),
child: TextField(
controller: ctrl,
autofocus: true,
@@ -838,7 +839,7 @@ class _WarehouseFormDialogState extends ConsumerState<_WarehouseFormDialog> {
return AlertDialog(
title: Text(widget.warehouse != null ? '编辑仓库' : '新建仓库'),
content: SizedBox(
width: 400,
width: context.dialogWidth(400),
child: Form(
key: _formKey,
child: Column(
@@ -971,7 +972,7 @@ class _UserFormDialogState extends ConsumerState<_UserFormDialog> {
return AlertDialog(
title: Text(_isEdit ? '编辑用户' : '新增用户'),
content: SizedBox(
width: 400,
width: context.dialogWidth(400),
child: Form(
key: _formKey,
child: Column(
@@ -1084,7 +1085,7 @@ class _ResetPasswordDialogState extends ConsumerState<_ResetPasswordDialog> {
return AlertDialog(
title: Text('重置密码 — ${widget.user.username}'),
content: SizedBox(
width: 360,
width: context.dialogWidth(360),
child: TextField(
controller: _ctrl,
obscureText: true,
@@ -1625,7 +1626,7 @@ class _BatchImportWidgetState extends ConsumerState<_BatchImportWidget> {
const Text('确认清空数据', style: TextStyle(color: Colors.red)),
]),
content: SizedBox(
width: 400,
width: context.dialogWidth(400),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
@@ -2035,7 +2036,7 @@ class _ShopEditDialogState extends ConsumerState<_ShopEditDialog> {
return AlertDialog(
title: const Text('编辑酒行信息'),
content: SizedBox(
width: 400,
width: context.dialogWidth(400),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
+147 -53
View File
@@ -6,6 +6,7 @@ import 'package:intl/intl.dart';
import 'dart:async';
import '../../core/auth/auth_state.dart';
import '../../core/config/app_config.dart';
import '../../core/responsive/responsive.dart';
import '../../core/theme/app_theme.dart';
import '../../providers/connectivity_provider.dart';
import '../../providers/shop_provider.dart';
@@ -24,6 +25,7 @@ class _AppShellState extends ConsumerState<AppShell> {
bool _sidebarExpanded = true;
final String _loginTime = DateFormat('HH:mm:ss').format(DateTime.now());
bool _forceDialogShown = false;
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
void _showForceUpdateDialog(
BuildContext context, AppUpdateInfo info) {
@@ -69,11 +71,86 @@ class _AppShellState extends ConsumerState<AppShell> {
_NavItem(icon: Icons.info_outline, label: '关于我们', path: '/about'),
];
/// 窄屏(手机)侧滑抽屉导航,容纳全部菜单项。
Widget _buildDrawer(
BuildContext context, AuthUser? user, String location) {
final shopAsync = ref.watch(shopInfoProvider);
final shopName = shopAsync.valueOrNull?.name ?? user?.shopNo ?? '';
final logoUrl = shopAsync.valueOrNull?.logoUrl ?? '';
return Drawer(
child: Container(
color: AppTheme.primaryDark,
child: SafeArea(
child: Column(
children: [
// 抽屉头部:门店 Logo + 名称 + 编号
Container(
width: double.infinity,
padding: const EdgeInsets.fromLTRB(16, 20, 16, 16),
color: AppTheme.primary,
child: Row(
children: [
_ShopLogo(
logoUrl: logoUrl, shopName: shopName, size: 40),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
shopName.isEmpty ? '' : shopName,
style: const TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.w600),
overflow: TextOverflow.ellipsis,
),
if (user != null) ...[
const SizedBox(height: 2),
Text(
'${user.shopNo} · ${user.username}',
style: const TextStyle(
color: Colors.white70, fontSize: 12),
overflow: TextOverflow.ellipsis,
),
],
],
),
),
],
),
),
Expanded(
child: ListView(
padding: const EdgeInsets.symmetric(vertical: 8),
children: _navItems.map((item) {
final isActive = location.startsWith(item.path);
return _SidebarItem(
item: item,
isActive: isActive,
expanded: true,
onTap: () {
Navigator.pop(context); // 关闭抽屉
context.go(item.path);
},
);
}).toList(),
),
),
],
),
),
),
);
}
@override
Widget build(BuildContext context) {
final user = ref.watch(authStateProvider).user;
final isOnline = ref.watch(connectivityProvider);
final location = GoRouterState.of(context).matchedLocation;
final isMobile = context.isMobile;
final sidebarWidth = _sidebarExpanded ? 200.0 : 56.0;
final updateNotifier = ref.watch(updateProvider.notifier);
final updateInfo = ref.watch(updateProvider).valueOrNull;
@@ -82,6 +159,8 @@ class _AppShellState extends ConsumerState<AppShell> {
return SelectionArea(
child: Scaffold(
key: _scaffoldKey,
drawer: isMobile ? _buildDrawer(context, user, location) : null,
body: Column(
children: [
// Top Bar
@@ -93,41 +172,54 @@ class _AppShellState extends ConsumerState<AppShell> {
children: [
IconButton(
icon: Icon(
_sidebarExpanded ? Icons.menu_open : Icons.menu,
isMobile
? Icons.menu
: (_sidebarExpanded ? Icons.menu_open : Icons.menu),
color: Colors.white),
onPressed: () =>
setState(() => _sidebarExpanded = !_sidebarExpanded),
tooltip: _sidebarExpanded ? '收起侧边栏' : '展开侧边栏',
onPressed: () {
if (isMobile) {
_scaffoldKey.currentState?.openDrawer();
} else {
setState(() => _sidebarExpanded = !_sidebarExpanded);
}
},
tooltip: isMobile
? '菜单'
: (_sidebarExpanded ? '收起侧边栏' : '展开侧边栏'),
),
const SizedBox(width: 4),
_ShopButton(user: user, version: appVersion),
const Spacer(),
if (user != null) ...[
MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
onTap: () => _showShopPanel(context, user, version: appVersion),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.business,
color: Colors.white70, size: 14),
const SizedBox(width: 4),
Text(user.shopNo,
style: const TextStyle(
color: Colors.white70, fontSize: 13)),
],
// 窄屏隐藏门店号/用户名文字块,仅保留用户菜单,避免顶栏拥挤
if (!isMobile) ...[
MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
onTap: () =>
_showShopPanel(context, user, version: appVersion),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.business,
color: Colors.white70, size: 14),
const SizedBox(width: 4),
Text(user.shopNo,
style: const TextStyle(
color: Colors.white70, fontSize: 13)),
],
),
),
),
),
const SizedBox(width: 20),
const Icon(Icons.person_outline,
color: Colors.white70, size: 14),
const SizedBox(width: 4),
Text(user.username,
style: const TextStyle(
color: Colors.white70, fontSize: 13)),
const SizedBox(width: 8),
const SizedBox(width: 20),
const Icon(Icons.person_outline,
color: Colors.white70, size: 14),
const SizedBox(width: 4),
Text(user.username,
style: const TextStyle(
color: Colors.white70, fontSize: 13)),
const SizedBox(width: 8),
],
PopupMenuButton<String>(
icon: const Icon(Icons.keyboard_arrow_down,
color: Colors.white70),
@@ -172,32 +264,33 @@ class _AppShellState extends ConsumerState<AppShell> {
Expanded(
child: Row(
children: [
// Sidebar
AnimatedContainer(
duration: const Duration(milliseconds: 200),
curve: Curves.easeInOut,
width: sidebarWidth,
color: AppTheme.primaryDark,
child: Column(
children: [
Expanded(
child: ListView(
padding: const EdgeInsets.symmetric(vertical: 8),
children: _navItems.map((item) {
final isActive =
location.startsWith(item.path);
return _SidebarItem(
item: item,
isActive: isActive,
expanded: _sidebarExpanded,
onTap: () => context.go(item.path),
);
}).toList(),
// Sidebar(仅宽屏;窄屏改用 Drawer
if (!isMobile)
AnimatedContainer(
duration: const Duration(milliseconds: 200),
curve: Curves.easeInOut,
width: sidebarWidth,
color: AppTheme.primaryDark,
child: Column(
children: [
Expanded(
child: ListView(
padding: const EdgeInsets.symmetric(vertical: 8),
children: _navItems.map((item) {
final isActive =
location.startsWith(item.path);
return _SidebarItem(
item: item,
isActive: isActive,
expanded: _sidebarExpanded,
onTap: () => context.go(item.path),
);
}).toList(),
),
),
),
],
],
),
),
),
// Content area
Expanded(
child: Column(
@@ -278,7 +371,8 @@ class _AppShellState extends ConsumerState<AppShell> {
),
),
Expanded(child: widget.child),
// Status bar
// Status bar(仅宽屏;窄屏隐藏,离线已有顶部横幅提示)
if (!isMobile)
LayoutBuilder(
builder: (context, constraints) {
final w = constraints.maxWidth;
@@ -515,7 +609,7 @@ void _showShopPanel(BuildContext context, AuthUser u, {String version = 'v1.0.0'
builder: (ctx) => Dialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
child: SizedBox(
width: 360,
width: ctx.dialogWidth(360),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../core/responsive/responsive.dart';
import 'package:go_router/go_router.dart';
import '../../core/theme/app_theme.dart';
import '../../core/auth/auth_state.dart';
@@ -849,8 +850,14 @@ class _FormField extends StatelessWidget {
@override
Widget build(BuildContext context) {
// 窄屏(手机)字段占满整行,便于点选;宽屏沿用固定宽度配合 Wrap 多列。
final effectiveWidth = width == double.infinity
? double.infinity
: (context.isMobile
? MediaQuery.sizeOf(context).width - 64
: width);
return SizedBox(
width: width,
width: effectiveWidth,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@@ -4,11 +4,13 @@ import '../../core/utils/print_util.dart' show safePrint, printStockInOrder, pri
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../../core/responsive/responsive.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/mobile_list_card.dart';
import '../../widgets/multi_select_dropdown.dart' show ColDef, ColumnToggleButton, FilterableColumnHeader;
import '../../widgets/page_scaffold.dart';
import '../../widgets/status_badge.dart';
@@ -239,88 +241,10 @@ class _StockInListScreenState extends ConsumerState<StockInListScreen> {
return DataCell(SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
TextButton(
onPressed: () => _showDetail(context, o.id),
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) {
await safePrint(context, () => 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}'),
child: const Text('修改',
style: TextStyle(
fontSize: 12, color: AppTheme.primary)),
),
TextButton(
onPressed: () => _confirmDelete(context, o),
child: const Text('删除',
style: TextStyle(
fontSize: 12, color: AppTheme.danger)),
),
TextButton(
onPressed: () => _confirmSubmit(context, o),
child: const Text('提交',
style: TextStyle(
fontSize: 12, color: AppTheme.primary)),
),
],
if (o.status == 'pending') ...[
TextButton(
key: Key('btn_approve_${o.id}'),
onPressed: () => _confirmApprove(context, o),
child: const Text('通过',
style: TextStyle(
fontSize: 12, color: AppTheme.success)),
),
TextButton(
key: Key('btn_reject_${o.id}'),
onPressed: () => _confirmReject(context, o),
child: const Text('拒绝',
style: TextStyle(
fontSize: 12, color: AppTheme.danger)),
),
],
],
)));
mainAxisSize: MainAxisSize.min,
children: _orderActions(context, o),
),
));
default:
return const DataCell(SizedBox());
}
@@ -414,6 +338,105 @@ class _StockInListScreenState extends ConsumerState<StockInListScreen> {
),
columns: columns,
rows: rows,
mobileCards: orders.map((o) => _orderCard(context, o)).toList(),
);
}
/// 操作按钮列表,表格与移动端卡片共用。
List<Widget> _orderActions(BuildContext context, StockInOrder o) {
return [
TextButton(
onPressed: () => _showDetail(context, o.id),
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) {
await safePrint(context, () => 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}'),
child: const Text('修改',
style: TextStyle(fontSize: 12, color: AppTheme.primary)),
),
TextButton(
onPressed: () => _confirmDelete(context, o),
child: const Text('删除',
style: TextStyle(fontSize: 12, color: AppTheme.danger)),
),
TextButton(
onPressed: () => _confirmSubmit(context, o),
child: const Text('提交',
style: TextStyle(fontSize: 12, color: AppTheme.primary)),
),
],
if (o.status == 'pending') ...[
TextButton(
key: Key('btn_approve_${o.id}'),
onPressed: () => _confirmApprove(context, o),
child: const Text('通过',
style: TextStyle(fontSize: 12, color: AppTheme.success)),
),
TextButton(
key: Key('btn_reject_${o.id}'),
onPressed: () => _confirmReject(context, o),
child: const Text('拒绝',
style: TextStyle(fontSize: 12, color: AppTheme.danger)),
),
],
];
}
/// 入库单:窄屏卡片
Widget _orderCard(BuildContext context, StockInOrder o) {
return MobileListCard(
onTap: () => _showDetail(context, o.id),
title: Text(o.orderNo,
style: const TextStyle(fontFamily: 'monospace', fontSize: 14)),
trailing: StatusBadge(_apiStatusToEnum(o.status)),
fields: [
MobileCardField('供应商', o.partnerName ?? '-'),
MobileCardField('仓库', o.warehouseName ?? '-'),
MobileCardField(
'金额',
o.totalAmount != null
? '¥${o.totalAmount!.toStringAsFixed(2)}'
: '-'),
MobileCardField('日期', o.orderDate?.substring(0, 10) ?? '-'),
],
actions: _orderActions(context, o),
);
}
@@ -668,7 +691,7 @@ class _StockInDetailDialogState extends ConsumerState<_StockInDetailDialog> {
Widget build(BuildContext context) {
return Dialog(
child: Container(
width: 720,
width: context.dialogWidth(720),
constraints: const BoxConstraints(maxHeight: 600),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -965,7 +988,7 @@ class _LabelPrintDialogState extends State<_LabelPrintDialog> {
final items = widget.order.items;
return Dialog(
child: Container(
width: 520,
width: context.dialogWidth(520),
constraints: const BoxConstraints(maxHeight: 520),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -2,6 +2,7 @@ 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/responsive/responsive.dart';
import '../../core/theme/app_theme.dart';
import '../../core/auth/auth_state.dart';
import '../../core/utils/print_util.dart';
@@ -683,8 +684,14 @@ class _FormField extends StatelessWidget {
@override
Widget build(BuildContext context) {
// 窄屏(手机)字段占满整行,便于点选;宽屏沿用固定宽度配合 Wrap 多列。
final effectiveWidth = width == double.infinity
? double.infinity
: (context.isMobile
? MediaQuery.sizeOf(context).width - 64
: width);
return SizedBox(
width: width,
width: effectiveWidth,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@@ -742,7 +749,7 @@ class _InventoryPickerDialogState extends State<_InventoryPickerDialog> {
return Dialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
child: SizedBox(
width: 820,
width: context.dialogWidth(820),
height: 580,
child: Column(
children: [
@@ -5,11 +5,13 @@ import '../../core/utils/print_util.dart' show safePrint, printStockOutOrder, pr
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../../core/responsive/responsive.dart';
import '../../core/theme/app_theme.dart';
import '../../models/stock_out.dart';
import '../../providers/stock_out_provider.dart';
import '../../repositories/stock_out_repository.dart';
import '../../widgets/data_table_card.dart';
import '../../widgets/mobile_list_card.dart';
import '../../widgets/multi_select_dropdown.dart' show ColDef, ColumnToggleButton, FilterableColumnHeader;
import '../../widgets/page_scaffold.dart';
import '../../widgets/status_badge.dart';
@@ -246,69 +248,10 @@ class _StockOutListScreenState extends ConsumerState<StockOutListScreen> {
return DataCell(SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
TextButton(
onPressed: () => _showDetail(context, o.id),
child: const Text('详情',
style:
TextStyle(fontSize: 12, color: AppTheme.primary)),
),
TextButton(
onPressed: () async {
final order = await ref.read(stockOutRepositoryProvider).get(o.id);
if (context.mounted) {
await safePrint(context, () => printStockOutOrder(order));
}
},
child: const Text('打印',
style: TextStyle(fontSize: 12, color: AppTheme.primary)),
),
if (o.status == 'approved') ...[
TextButton(
onPressed: () => _confirmSettle(context, o.id, 'stock_out'),
child: const Text('结清',
style: TextStyle(fontSize: 12, color: AppTheme.accent)),
),
],
if (o.status == 'draft') ...[
TextButton(
onPressed: () => context.go('/stock-out/edit/${o.id}'),
child: const Text('修改',
style: TextStyle(
fontSize: 12, color: AppTheme.primary)),
),
TextButton(
onPressed: () => _confirmDelete(context, o),
child: const Text('删除',
style: TextStyle(
fontSize: 12, color: AppTheme.danger)),
),
TextButton(
onPressed: () => _confirmSubmit(context, o),
child: const Text('提交',
style: TextStyle(
fontSize: 12, color: AppTheme.primary)),
),
],
if (o.status == 'pending') ...[
TextButton(
key: Key('btn_approve_${o.id}'),
onPressed: () => _confirmApprove(context, o),
child: const Text('通过',
style: TextStyle(
fontSize: 12, color: AppTheme.success)),
),
TextButton(
key: Key('btn_reject_${o.id}'),
onPressed: () => _confirmReject(context, o),
child: const Text('拒绝',
style: TextStyle(
fontSize: 12, color: AppTheme.danger)),
),
],
],
)));
mainAxisSize: MainAxisSize.min,
children: _orderActions(context, o),
),
));
default:
return const DataCell(SizedBox());
}
@@ -402,6 +345,86 @@ class _StockOutListScreenState extends ConsumerState<StockOutListScreen> {
),
columns: columns,
rows: rows,
mobileCards: orders.map((o) => _orderCard(context, o)).toList(),
);
}
/// 操作按钮列表,表格与移动端卡片共用。
List<Widget> _orderActions(BuildContext context, StockOutOrder o) {
return [
TextButton(
onPressed: () => _showDetail(context, o.id),
child: const Text('详情',
style: TextStyle(fontSize: 12, color: AppTheme.primary)),
),
TextButton(
onPressed: () async {
final order = await ref.read(stockOutRepositoryProvider).get(o.id);
if (context.mounted) {
await safePrint(context, () => printStockOutOrder(order));
}
},
child: const Text('打印',
style: TextStyle(fontSize: 12, color: AppTheme.primary)),
),
if (o.status == 'approved')
TextButton(
onPressed: () => _confirmSettle(context, o.id, 'stock_out'),
child: const Text('结清',
style: TextStyle(fontSize: 12, color: AppTheme.accent)),
),
if (o.status == 'draft') ...[
TextButton(
onPressed: () => context.go('/stock-out/edit/${o.id}'),
child: const Text('修改',
style: TextStyle(fontSize: 12, color: AppTheme.primary)),
),
TextButton(
onPressed: () => _confirmDelete(context, o),
child: const Text('删除',
style: TextStyle(fontSize: 12, color: AppTheme.danger)),
),
TextButton(
onPressed: () => _confirmSubmit(context, o),
child: const Text('提交',
style: TextStyle(fontSize: 12, color: AppTheme.primary)),
),
],
if (o.status == 'pending') ...[
TextButton(
key: Key('btn_approve_${o.id}'),
onPressed: () => _confirmApprove(context, o),
child: const Text('通过',
style: TextStyle(fontSize: 12, color: AppTheme.success)),
),
TextButton(
key: Key('btn_reject_${o.id}'),
onPressed: () => _confirmReject(context, o),
child: const Text('拒绝',
style: TextStyle(fontSize: 12, color: AppTheme.danger)),
),
],
];
}
/// 出库单:窄屏卡片
Widget _orderCard(BuildContext context, StockOutOrder o) {
return MobileListCard(
onTap: () => _showDetail(context, o.id),
title: Text(o.orderNo,
style: const TextStyle(fontFamily: 'monospace', fontSize: 14)),
trailing: StatusBadge(_apiStatusToEnum(o.status)),
fields: [
MobileCardField('客户', o.partnerName ?? '-'),
MobileCardField('仓库', o.warehouseName ?? '-'),
MobileCardField(
'金额',
o.totalAmount != null
? '¥${o.totalAmount!.toStringAsFixed(2)}'
: '-'),
MobileCardField('日期', o.orderDate?.substring(0, 10) ?? '-'),
],
actions: _orderActions(context, o),
);
}
@@ -660,7 +683,7 @@ class _StockOutDetailDialogState extends ConsumerState<_StockOutDetailDialog> {
Widget build(BuildContext context) {
return Dialog(
child: Container(
width: 720,
width: context.dialogWidth(720),
constraints: const BoxConstraints(maxHeight: 600),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -961,7 +984,7 @@ class _LabelPrintDialogState extends State<_LabelPrintDialog> {
final items = widget.order.items;
return Dialog(
child: Container(
width: 520,
width: context.dialogWidth(520),
constraints: const BoxConstraints(maxHeight: 520),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,