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:
wangjia
2026-06-07 20:16:34 +08:00
parent e550b73d6d
commit ced407ea87
7 changed files with 975 additions and 678 deletions
@@ -3,7 +3,9 @@ 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/warehouse.dart';
import '../../providers/product_option_provider.dart';
import '../../providers/warehouse_provider.dart';
import '../../widgets/data_table_card.dart';
import '../../widgets/mobile_list_card.dart';
import '../../widgets/page_scaffold.dart';
@@ -41,11 +43,13 @@ class _ProductsScreenState extends ConsumerState<ProductsScreen> {
Tab(text: '商品名称'),
Tab(text: '系列'),
Tab(text: '规格'),
Tab(text: '仓库'),
],
tabViews: [
_buildNameTab(),
_buildSeriesTab(),
_buildSpecTab(),
_buildWarehousesTab(),
],
);
}
@@ -451,6 +455,123 @@ class _ProductsScreenState extends ConsumerState<ProductsScreen> {
}
}
// ── 仓库 tab ──────────────────────────────────────────────
Widget _buildWarehousesTab() {
final asyncWarehouses = ref.watch(warehouseListProvider);
return asyncWarehouses.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (e, _) => _buildError(() => ref.read(warehouseListProvider.notifier).reload()),
data: (warehouses) => DataTableCard(
totalCount: warehouses.length,
page: 1,
pageSize: warehouses.length + 1,
onPageChanged: (_) {},
onPageSizeChanged: (_) {},
toolbar: Row(
children: [
ElevatedButton.icon(
onPressed: () => _showWarehouseDialog(),
icon: const Icon(Icons.add, size: 16),
label: const Text('新建'),
),
],
),
mobileCards: warehouses
.map((w) => MobileListCard(
title: Text(w.name),
subtitle: w.location?.isNotEmpty == true ? Text(w.location!) : null,
fields: [
if (w.isDefault) const MobileCardField('默认仓库', ''),
],
actions: [
TextButton(
onPressed: () => _showWarehouseDialog(warehouse: w),
child: const Text('编辑', style: TextStyle(fontSize: 13)),
),
TextButton(
onPressed: () => _confirmDeleteWarehouse(w),
child: const Text('删除',
style: TextStyle(fontSize: 13, color: AppTheme.danger)),
),
],
))
.toList(),
columns: const [
DataColumn(label: Text('仓库名称')),
DataColumn(label: Text('位置')),
DataColumn(label: Text('默认仓库')),
DataColumn(label: Text('操作')),
],
rows: warehouses.isEmpty
? [const DataRow(cells: [
DataCell(SizedBox()),
DataCell(Text('暂无仓库', style: TextStyle(color: AppTheme.textSecondary))),
DataCell(SizedBox()),
DataCell(SizedBox()),
])]
: warehouses
.map((w) => DataRow(cells: [
DataCell(Text(w.name, style: const TextStyle(fontWeight: FontWeight.w500))),
DataCell(Text(w.location ?? '-')),
DataCell(w.isDefault
? const Icon(Icons.check_circle, color: AppTheme.success, size: 18)
: const SizedBox()),
DataCell(_actionButtons(
onEdit: () => _showWarehouseDialog(warehouse: w),
onDelete: () => _confirmDeleteWarehouse(w),
)),
]))
.toList(),
),
);
}
void _showWarehouseDialog({Warehouse? warehouse}) {
showAppDialog(
context: context,
builder: (ctx) => _WarehouseFormDialog(
warehouse: warehouse,
onSaved: () => ref.read(warehouseListProvider.notifier).reload(),
),
);
}
Future<void> _confirmDeleteWarehouse(Warehouse w) async {
final confirmed = await showDialog<bool>(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('确认删除'),
content: Text('确认删除仓库「${w.name}」?'),
actions: [
TextButton(
onPressed: () => Navigator.of(ctx).pop(false),
child: const Text('取消')),
ElevatedButton(
onPressed: () => Navigator.of(ctx).pop(true),
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.danger, foregroundColor: Colors.white),
child: const Text('删除'),
),
],
),
);
if (confirmed == true && mounted) {
try {
await ref.read(warehouseListProvider.notifier).deleteWarehouse(w.id);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('删除成功'), backgroundColor: AppTheme.success));
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('删除失败:$e'), backgroundColor: AppTheme.danger));
}
}
}
}
Future<void> _showOptionDialog({
required String title,
required bool hasQuantity,
@@ -535,3 +656,119 @@ class _ProductsScreenState extends ConsumerState<ProductsScreen> {
);
}
}
class _WarehouseFormDialog extends ConsumerStatefulWidget {
final Warehouse? warehouse;
final VoidCallback onSaved;
const _WarehouseFormDialog({this.warehouse, required this.onSaved});
@override
ConsumerState<_WarehouseFormDialog> createState() => _WarehouseFormDialogState();
}
class _WarehouseFormDialogState extends ConsumerState<_WarehouseFormDialog> {
final _formKey = GlobalKey<FormState>();
late final TextEditingController _nameCtrl;
late final TextEditingController _locationCtrl;
late bool _isDefault;
bool _saving = false;
@override
void initState() {
super.initState();
_nameCtrl = TextEditingController(text: widget.warehouse?.name ?? '');
_locationCtrl = TextEditingController(text: widget.warehouse?.location ?? '');
_isDefault = widget.warehouse?.isDefault ?? false;
}
@override
void dispose() {
_nameCtrl.dispose();
_locationCtrl.dispose();
super.dispose();
}
Future<void> _save() async {
if (!_formKey.currentState!.validate()) return;
setState(() => _saving = true);
final data = {
'name': _nameCtrl.text.trim(),
if (_locationCtrl.text.trim().isNotEmpty) 'location': _locationCtrl.text.trim(),
'is_default': _isDefault,
};
try {
final notifier = ref.read(warehouseListProvider.notifier);
if (widget.warehouse != null) {
await notifier.updateWarehouse(widget.warehouse!.id, data);
} else {
await notifier.createWarehouse(data);
}
if (mounted) {
Navigator.of(context).pop();
widget.onSaved();
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text(widget.warehouse != null ? '仓库更新成功' : '仓库创建成功'),
backgroundColor: AppTheme.success,
));
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('保存失败:$e'), backgroundColor: AppTheme.danger));
}
} finally {
if (mounted) setState(() => _saving = false);
}
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Text(widget.warehouse != null ? '编辑仓库' : '新建仓库'),
content: SizedBox(
width: context.dialogWidth(400),
child: Form(
key: _formKey,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextFormField(
controller: _nameCtrl,
decoration: const InputDecoration(labelText: '仓库名称'),
validator: (v) => (v == null || v.isEmpty) ? '不能为空' : null,
),
const SizedBox(height: 12),
TextFormField(
controller: _locationCtrl,
decoration: const InputDecoration(labelText: '位置'),
),
const SizedBox(height: 12),
CheckboxListTile(
title: const Text('设为默认仓库'),
value: _isDefault,
onChanged: (v) => setState(() => _isDefault = v ?? false),
contentPadding: EdgeInsets.zero,
),
],
),
),
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('取消'),
),
ElevatedButton(
onPressed: _saving ? null : _save,
child: _saving
? const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(strokeWidth: 2))
: const Text('保存'),
),
],
);
}
}