feat(client): 移动端布局适配 + 信息架构整理
- 入库/出库详情页:窄屏商品明细切换为卡片竖排,顶部操作收入溢出菜单 - 基础数据:新增「仓库」tab,使用 DataTableCard + mobileCards 适配窄屏 - 系统设置:移除仓库 tab(已迁入基础数据),新增「授权」tab,数据导入改名「数据管理」 - 关于我们:移除授权信息,补充帮助与文档/扫码防伪/法律信息/系统信息等模块 - AppInfo 新增 phone/wechat/termsUrl/privacyUrl/docsUrl 字段(可选,空则不渲染) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -11,6 +11,7 @@ import '../../providers/inventory_provider.dart';
|
||||
import '../../providers/partner_provider.dart';
|
||||
import '../../providers/stock_out_provider.dart';
|
||||
import '../../providers/warehouse_provider.dart';
|
||||
import '../../widgets/mobile_list_card.dart';
|
||||
|
||||
// Aggregated per-product inventory item for the picker dialog
|
||||
class _PickerItem {
|
||||
@@ -307,6 +308,7 @@ class _StockOutFormScreenState extends ConsumerState<StockOutFormScreen> {
|
||||
final asyncWarehouses = ref.watch(warehouseListProvider);
|
||||
final asyncCustomers = ref.watch(customerListProvider);
|
||||
final currentUser = ref.watch(authStateProvider).user;
|
||||
final isMobile = context.isMobile;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: AppTheme.background,
|
||||
@@ -327,35 +329,71 @@ class _StockOutFormScreenState extends ConsumerState<StockOutFormScreen> {
|
||||
Text(_isEdit ? '修改出库单' : '新建出库单',
|
||||
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600)),
|
||||
const Spacer(),
|
||||
if (_isEdit && _loadedOrder != null) ...[
|
||||
OutlinedButton.icon(
|
||||
onPressed: _printOrder,
|
||||
icon: const Icon(Icons.print_outlined, size: 16),
|
||||
label: const Text('打印'),
|
||||
// 窄屏:仅保留主操作「提交」,其余收进溢出菜单,避免按钮挤爆顶栏
|
||||
if (isMobile) ...[
|
||||
ElevatedButton(
|
||||
onPressed: _submitting ? null : () => _submit(false),
|
||||
child: _submitting
|
||||
? const SizedBox(
|
||||
width: 14,
|
||||
height: 14,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2, color: Colors.white))
|
||||
: const Text('提交'),
|
||||
),
|
||||
PopupMenuButton<String>(
|
||||
onSelected: (v) {
|
||||
switch (v) {
|
||||
case 'draft':
|
||||
_submit(true);
|
||||
break;
|
||||
case 'print':
|
||||
_printOrder();
|
||||
break;
|
||||
case 'cancel':
|
||||
context.go('/stock-out');
|
||||
break;
|
||||
}
|
||||
},
|
||||
itemBuilder: (_) => [
|
||||
const PopupMenuItem(value: 'draft', child: Text('保存草稿')),
|
||||
if (_isEdit && _loadedOrder != null)
|
||||
const PopupMenuItem(value: 'print', child: Text('打印')),
|
||||
const PopupMenuItem(value: 'cancel', child: Text('取消')),
|
||||
],
|
||||
),
|
||||
] else ...[
|
||||
if (_isEdit && _loadedOrder != null) ...[
|
||||
OutlinedButton.icon(
|
||||
onPressed: _printOrder,
|
||||
icon: const Icon(Icons.print_outlined, size: 16),
|
||||
label: const Text('打印'),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
],
|
||||
OutlinedButton(
|
||||
onPressed: _submitting ? null : () => _submit(true),
|
||||
child: const Text('保存草稿'),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
ElevatedButton.icon(
|
||||
onPressed: _submitting ? null : () => _submit(false),
|
||||
icon: _submitting
|
||||
? const SizedBox(
|
||||
width: 14,
|
||||
height: 14,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2, color: Colors.white))
|
||||
: const Icon(Icons.send, size: 16),
|
||||
label: const Text('提交审核'),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
OutlinedButton.icon(
|
||||
onPressed: () => context.go('/stock-out'),
|
||||
icon: const Icon(Icons.cancel_outlined, size: 16),
|
||||
label: const Text('取消'),
|
||||
),
|
||||
],
|
||||
OutlinedButton(
|
||||
onPressed: _submitting ? null : () => _submit(true),
|
||||
child: const Text('保存草稿'),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
ElevatedButton.icon(
|
||||
onPressed: _submitting ? null : () => _submit(false),
|
||||
icon: _submitting
|
||||
? const SizedBox(
|
||||
width: 14,
|
||||
height: 14,
|
||||
child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white))
|
||||
: const Icon(Icons.send, size: 16),
|
||||
label: const Text('提交审核'),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
OutlinedButton.icon(
|
||||
onPressed: () => context.go('/stock-out'),
|
||||
icon: const Icon(Icons.cancel_outlined, size: 16),
|
||||
label: const Text('取消'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -507,38 +545,50 @@ class _StockOutFormScreenState extends ConsumerState<StockOutFormScreen> {
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Table(
|
||||
columnWidths: const {
|
||||
0: FixedColumnWidth(36), // 序号
|
||||
1: FlexColumnWidth(1.2), // 商品编码
|
||||
2: FlexColumnWidth(2.0), // 商品名称
|
||||
3: FlexColumnWidth(1.2), // 系列
|
||||
4: FlexColumnWidth(1.2), // 规格
|
||||
5: FlexColumnWidth(1.0), // 单价
|
||||
6: FlexColumnWidth(0.8), // 数量
|
||||
7: FlexColumnWidth(1.0), // 金额
|
||||
8: FixedColumnWidth(48), // 操作
|
||||
},
|
||||
children: [
|
||||
TableRow(
|
||||
decoration: const BoxDecoration(color: Color(0xFFF0F4FF)),
|
||||
children: [
|
||||
'序号', '商品编码', '商品名称', '系列', '规格', '单价', '数量', '金额', '操作',
|
||||
]
|
||||
.map((h) => Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8, vertical: 10),
|
||||
child: Text(h,
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppTheme.primaryDark)),
|
||||
))
|
||||
.toList(),
|
||||
),
|
||||
...List.generate(_items.length, (i) => _buildItemRow(i)),
|
||||
],
|
||||
),
|
||||
// 窄屏:逐项卡片竖排,避免 9 列表格横向溢出;宽屏保持表格
|
||||
if (isMobile)
|
||||
Column(
|
||||
children: List.generate(
|
||||
_items.length,
|
||||
(i) => Padding(
|
||||
padding:
|
||||
const EdgeInsets.only(bottom: 10),
|
||||
child: _buildItemCard(i),
|
||||
)),
|
||||
)
|
||||
else
|
||||
Table(
|
||||
columnWidths: const {
|
||||
0: FixedColumnWidth(36), // 序号
|
||||
1: FlexColumnWidth(1.2), // 商品编码
|
||||
2: FlexColumnWidth(2.0), // 商品名称
|
||||
3: FlexColumnWidth(1.2), // 系列
|
||||
4: FlexColumnWidth(1.2), // 规格
|
||||
5: FlexColumnWidth(1.0), // 单价
|
||||
6: FlexColumnWidth(0.8), // 数量
|
||||
7: FlexColumnWidth(1.0), // 金额
|
||||
8: FixedColumnWidth(48), // 操作
|
||||
},
|
||||
children: [
|
||||
TableRow(
|
||||
decoration: const BoxDecoration(color: Color(0xFFF0F4FF)),
|
||||
children: [
|
||||
'序号', '商品编码', '商品名称', '系列', '规格', '单价', '数量', '金额', '操作',
|
||||
]
|
||||
.map((h) => Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8, vertical: 10),
|
||||
child: Text(h,
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppTheme.primaryDark)),
|
||||
))
|
||||
.toList(),
|
||||
),
|
||||
...List.generate(_items.length, (i) => _buildItemRow(i)),
|
||||
],
|
||||
),
|
||||
if (_items.isEmpty)
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 24),
|
||||
@@ -583,7 +633,6 @@ class _StockOutFormScreenState extends ConsumerState<StockOutFormScreen> {
|
||||
final qty = double.tryParse(item.qtyCtrl.text) ?? 0;
|
||||
final price = item.unitPrice ?? 0;
|
||||
final amount = qty * price;
|
||||
final available = _inventoryMap[item.productId];
|
||||
|
||||
return TableRow(
|
||||
decoration: BoxDecoration(
|
||||
@@ -603,22 +652,7 @@ class _StockOutFormScreenState extends ConsumerState<StockOutFormScreen> {
|
||||
// 单价
|
||||
_cell(Text(price > 0 ? '¥${price.toStringAsFixed(2)}' : '-', style: const TextStyle(fontSize: 13))),
|
||||
// 数量
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(4),
|
||||
child: TextFormField(
|
||||
controller: item.qtyCtrl,
|
||||
decoration: const InputDecoration(hintText: '0', isDense: true),
|
||||
style: const TextStyle(fontSize: 13),
|
||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||
inputFormatters: [FilteringTextInputFormatter.allow(RegExp(r'^\d+\.?\d{0,2}'))],
|
||||
onChanged: (_) => setState(() {}),
|
||||
validator: (v) {
|
||||
if (v == null || v.isEmpty) return '不能为空';
|
||||
if ((double.tryParse(v) ?? 0) <= 0) return '>0';
|
||||
return null;
|
||||
},
|
||||
),
|
||||
),
|
||||
Padding(padding: const EdgeInsets.all(4), child: _qtyField(item)),
|
||||
// 金额
|
||||
_cell(Text('¥${amount.toStringAsFixed(2)}', style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w500))),
|
||||
// 操作
|
||||
@@ -636,6 +670,48 @@ class _StockOutFormScreenState extends ConsumerState<StockOutFormScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _qtyField(_ItemRow item) {
|
||||
return TextFormField(
|
||||
controller: item.qtyCtrl,
|
||||
decoration: const InputDecoration(hintText: '0', isDense: true),
|
||||
style: const TextStyle(fontSize: 13),
|
||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||
inputFormatters: [FilteringTextInputFormatter.allow(RegExp(r'^\d+\.?\d{0,2}'))],
|
||||
onChanged: (_) => setState(() {}),
|
||||
validator: (v) {
|
||||
if (v == null || v.isEmpty) return '不能为空';
|
||||
if ((double.tryParse(v) ?? 0) <= 0) return '>0';
|
||||
return null;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// 窄屏(手机):每个明细行渲染为一张卡片,字段竖排,避免表格横向溢出。
|
||||
Widget _buildItemCard(int index) {
|
||||
final item = _items[index];
|
||||
final qty = double.tryParse(item.qtyCtrl.text) ?? 0;
|
||||
final price = item.unitPrice ?? 0;
|
||||
final amount = qty * price;
|
||||
|
||||
return MobileListCard(
|
||||
title: Text(item.productName),
|
||||
subtitle: item.productCode.isNotEmpty ? Text('编码 ${item.productCode}') : null,
|
||||
trailing: IconButton(
|
||||
icon: const Icon(Icons.delete_outline, size: 20, color: AppTheme.danger),
|
||||
onPressed: () => _removeItem(index),
|
||||
tooltip: '删除',
|
||||
visualDensity: VisualDensity.compact,
|
||||
),
|
||||
fields: [
|
||||
if (item.series.isNotEmpty) MobileCardField('系列', item.series),
|
||||
if (item.spec.isNotEmpty) MobileCardField('规格', item.spec),
|
||||
MobileCardField('单价', price > 0 ? '¥${price.toStringAsFixed(2)}' : '-'),
|
||||
MobileCardField('数量', null, valueWidget: _qtyField(item)),
|
||||
MobileCardField('金额', '¥${amount.toStringAsFixed(2)}'),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _cell(Widget child) => Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 12),
|
||||
child: child,
|
||||
|
||||
Reference in New Issue
Block a user