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
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:
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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: [
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user