feat(client): 全模块 API 对接 + 修复登陆跳转 + 菜单 UI 优化

API 对接:
- 入库/出库/库存/财务/往来单位/基础数据全部对接后端 REST API
- 新增 repositories、providers、models 层,统一分层架构
- auth 从 flutter_secure_storage 迁移到 shared_preferences

登陆跳转修复:
- 将 _RouterNotifier 提取为独立 Riverpod provider,appRouterProvider
  使用 ref.read 避免依赖链导致 router 重建后跳回 /login
- redirect 函数新增 initialized 守卫,防止 auth 未恢复时误重定向
- 添加调试日志(Router/Auth/ApiClient)定位 401 触发的 logout 链路

退出菜单 UI:
- 去掉 ListTile,改用 Row + 自定义 padding,文字左对齐
- MouseRegion + AnimatedContainer 实现 hover 高亮(普通项蓝底/退出红底)
- 菜单圆角 6px,elevation 8,分割线高度 1px

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-04-07 22:20:28 +08:00
parent 31ea370cea
commit d717fb3735
70 changed files with 6919 additions and 2580 deletions
+88
View File
@@ -0,0 +1,88 @@
class Inventory {
final int warehouseId;
final String? warehouseName;
final int productId;
final String? productName;
final String? productCode;
final String? productSpec;
final String? productUnit;
final String? productBrand;
final int? minStock;
final double quantity;
const Inventory({
required this.warehouseId,
this.warehouseName,
required this.productId,
this.productName,
this.productCode,
this.productSpec,
this.productUnit,
this.productBrand,
this.minStock,
required this.quantity,
});
factory Inventory.fromJson(Map<String, dynamic> json) => Inventory(
warehouseId: (json['warehouse_id'] as num).toInt(),
warehouseName: json['warehouse_name'] as String?,
productId: (json['product_id'] as num).toInt(),
productName: json['product_name'] as String?,
productCode: json['product_code'] as String?,
productSpec: json['product_spec'] as String?,
productUnit: json['product_unit'] as String?,
productBrand: json['product_brand'] as String?,
minStock: json['min_stock'] != null
? (json['min_stock'] as num).toInt()
: null,
quantity: (json['quantity'] as num).toDouble(),
);
}
class InventoryLog {
final int warehouseId;
final String? warehouseName;
final int productId;
final String? productName;
final String direction; // in | out
final double quantity;
final double? qtyBefore;
final double? qtyAfter;
final String? refType;
final int? refId;
final String? createdAt;
const InventoryLog({
required this.warehouseId,
this.warehouseName,
required this.productId,
this.productName,
required this.direction,
required this.quantity,
this.qtyBefore,
this.qtyAfter,
this.refType,
this.refId,
this.createdAt,
});
factory InventoryLog.fromJson(Map<String, dynamic> json) => InventoryLog(
warehouseId: (json['warehouse_id'] as num).toInt(),
warehouseName: json['warehouse_name'] as String?,
productId: (json['product_id'] as num).toInt(),
productName: json['product_name'] as String?,
direction: json['direction'] as String,
quantity: (json['quantity'] as num).toDouble(),
qtyBefore: json['qty_before'] != null
? (json['qty_before'] as num).toDouble()
: null,
qtyAfter: json['qty_after'] != null
? (json['qty_after'] as num).toDouble()
: null,
refType: json['ref_type'] as String?,
refId: json['ref_id'] != null
? (json['ref_id'] as num).toInt()
: null,
createdAt: json['created_at'] as String?,
);
}
+46
View File
@@ -0,0 +1,46 @@
class Partner {
final int id;
final String? code;
final String name;
final String type; // supplier | customer
final String? contact;
final String? phone;
final String? address;
final String? bankAccount;
final String? remark;
const Partner({
required this.id,
this.code,
required this.name,
required this.type,
this.contact,
this.phone,
this.address,
this.bankAccount,
this.remark,
});
factory Partner.fromJson(Map<String, dynamic> json) => Partner(
id: (json['id'] as num).toInt(),
code: json['code'] as String?,
name: json['name'] as String,
type: json['type'] as String,
contact: json['contact'] as String?,
phone: json['phone'] as String?,
address: json['address'] as String?,
bankAccount: json['bank_account'] as String?,
remark: json['remark'] as String?,
);
Map<String, dynamic> toJson() => {
'name': name,
'type': type,
if (code != null) 'code': code,
if (contact != null) 'contact': contact,
if (phone != null) 'phone': phone,
if (address != null) 'address': address,
if (bankAccount != null) 'bank_account': bankAccount,
if (remark != null) 'remark': remark,
};
}
+73
View File
@@ -0,0 +1,73 @@
class Product {
final int id;
final String code;
final String? barcode;
final String name;
final String? series;
final String? spec;
final String unit;
final int? categoryId;
final String? brand;
final double? purchasePrice;
final double? salePrice;
final int? minStock;
final String? remark;
final Map<String, dynamic>? customFields;
const Product({
required this.id,
required this.code,
this.barcode,
required this.name,
this.series,
this.spec,
required this.unit,
this.categoryId,
this.brand,
this.purchasePrice,
this.salePrice,
this.minStock,
this.remark,
this.customFields,
});
factory Product.fromJson(Map<String, dynamic> json) => Product(
id: (json['id'] as num).toInt(),
code: json['code'] as String? ?? '',
barcode: json['barcode'] as String?,
name: json['name'] as String,
series: json['series'] as String?,
spec: json['spec'] as String?,
unit: json['unit'] as String? ?? '',
categoryId: json['category_id'] != null
? (json['category_id'] as num).toInt()
: null,
brand: json['brand'] as String?,
purchasePrice: json['purchase_price'] != null
? (json['purchase_price'] as num).toDouble()
: null,
salePrice: json['sale_price'] != null
? (json['sale_price'] as num).toDouble()
: null,
minStock: json['min_stock'] != null
? (json['min_stock'] as num).toInt()
: null,
remark: json['remark'] as String?,
customFields: json['custom_fields'] as Map<String, dynamic>?,
);
Map<String, dynamic> toJson() => {
'code': code,
if (barcode != null) 'barcode': barcode,
'name': name,
if (series != null) 'series': series,
if (spec != null) 'spec': spec,
'unit': unit,
if (categoryId != null) 'category_id': categoryId,
if (brand != null) 'brand': brand,
if (purchasePrice != null) 'purchase_price': purchasePrice,
if (salePrice != null) 'sale_price': salePrice,
if (minStock != null) 'min_stock': minStock,
if (remark != null) 'remark': remark,
};
}
+108
View File
@@ -0,0 +1,108 @@
class StockInItem {
final int? orderId;
final int productId;
final double quantity;
final double unitPrice;
final double totalPrice;
final String? batchNo;
// Denormalized for display
final String? productName;
final String? productCode;
final String? productSpec;
final String? productUnit;
const StockInItem({
this.orderId,
required this.productId,
required this.quantity,
required this.unitPrice,
required this.totalPrice,
this.batchNo,
this.productName,
this.productCode,
this.productSpec,
this.productUnit,
});
factory StockInItem.fromJson(Map<String, dynamic> json) => StockInItem(
orderId: json['order_id'] != null
? (json['order_id'] as num).toInt()
: null,
productId: (json['product_id'] as num).toInt(),
quantity: (json['quantity'] as num).toDouble(),
unitPrice: (json['unit_price'] as num).toDouble(),
totalPrice: (json['total_price'] as num).toDouble(),
batchNo: json['batch_no'] as String?,
productName: json['product_name'] as String?,
productCode: json['product_code'] as String?,
productSpec: json['product_spec'] as String?,
productUnit: json['product_unit'] as String?,
);
Map<String, dynamic> toJson() => {
'product_id': productId,
'quantity': quantity,
'unit_price': unitPrice,
'total_price': totalPrice,
if (batchNo != null) 'batch_no': batchNo,
};
}
class StockInOrder {
final int id;
final String orderNo;
final String? type;
final int warehouseId;
final String? warehouseName;
final int? partnerId;
final String? partnerName;
final int? operatorId;
final String status; // draft | pending | approved | rejected
final String? orderDate;
final double? totalAmount;
final String? remark;
final List<StockInItem> items;
const StockInOrder({
required this.id,
required this.orderNo,
this.type,
required this.warehouseId,
this.warehouseName,
this.partnerId,
this.partnerName,
this.operatorId,
required this.status,
this.orderDate,
this.totalAmount,
this.remark,
this.items = const [],
});
factory StockInOrder.fromJson(Map<String, dynamic> json) => StockInOrder(
id: (json['id'] as num).toInt(),
orderNo: json['order_no'] as String,
type: json['type'] as String?,
warehouseId: (json['warehouse_id'] as num).toInt(),
warehouseName: json['warehouse_name'] as String?,
partnerId: json['partner_id'] != null
? (json['partner_id'] as num).toInt()
: null,
partnerName: json['partner_name'] as String?,
operatorId: json['operator_id'] != null
? (json['operator_id'] as num).toInt()
: null,
status: json['status'] as String,
orderDate: json['order_date'] as String?,
totalAmount: json['total_amount'] != null
? (json['total_amount'] as num).toDouble()
: null,
remark: json['remark'] as String?,
items: json['items'] != null
? (json['items'] as List)
.map((e) =>
StockInItem.fromJson(e as Map<String, dynamic>))
.toList()
: [],
);
}
+96
View File
@@ -0,0 +1,96 @@
class StockOutItem {
final int? orderId;
final int productId;
final double quantity;
final double unitPrice;
final double totalPrice;
final String? productName;
final String? productCode;
final String? productSpec;
final String? productUnit;
const StockOutItem({
this.orderId,
required this.productId,
required this.quantity,
required this.unitPrice,
required this.totalPrice,
this.productName,
this.productCode,
this.productSpec,
this.productUnit,
});
factory StockOutItem.fromJson(Map<String, dynamic> json) => StockOutItem(
orderId: json['order_id'] != null
? (json['order_id'] as num).toInt()
: null,
productId: (json['product_id'] as num).toInt(),
quantity: (json['quantity'] as num).toDouble(),
unitPrice: (json['unit_price'] as num).toDouble(),
totalPrice: (json['total_price'] as num).toDouble(),
productName: json['product_name'] as String?,
productCode: json['product_code'] as String?,
productSpec: json['product_spec'] as String?,
productUnit: json['product_unit'] as String?,
);
}
class StockOutOrder {
final int id;
final String orderNo;
final String? type;
final int warehouseId;
final String? warehouseName;
final int? partnerId;
final String? partnerName;
final int? operatorId;
final String status; // draft | pending | approved | rejected
final String? orderDate;
final double? totalAmount;
final String? remark;
final List<StockOutItem> items;
const StockOutOrder({
required this.id,
required this.orderNo,
this.type,
required this.warehouseId,
this.warehouseName,
this.partnerId,
this.partnerName,
this.operatorId,
required this.status,
this.orderDate,
this.totalAmount,
this.remark,
this.items = const [],
});
factory StockOutOrder.fromJson(Map<String, dynamic> json) => StockOutOrder(
id: (json['id'] as num).toInt(),
orderNo: json['order_no'] as String,
type: json['type'] as String?,
warehouseId: (json['warehouse_id'] as num).toInt(),
warehouseName: json['warehouse_name'] as String?,
partnerId: json['partner_id'] != null
? (json['partner_id'] as num).toInt()
: null,
partnerName: json['partner_name'] as String?,
operatorId: json['operator_id'] != null
? (json['operator_id'] as num).toInt()
: null,
status: json['status'] as String,
orderDate: json['order_date'] as String?,
totalAmount: json['total_amount'] != null
? (json['total_amount'] as num).toDouble()
: null,
remark: json['remark'] as String?,
items: json['items'] != null
? (json['items'] as List)
.map((e) =>
StockOutItem.fromJson(e as Map<String, dynamic>))
.toList()
: [],
);
}
+26
View File
@@ -0,0 +1,26 @@
class Warehouse {
final int id;
final String name;
final String? location;
final bool isDefault;
const Warehouse({
required this.id,
required this.name,
this.location,
required this.isDefault,
});
factory Warehouse.fromJson(Map<String, dynamic> json) => Warehouse(
id: (json['id'] as num).toInt(),
name: json['name'] as String,
location: json['location'] as String?,
isDefault: json['is_default'] as bool? ?? false,
);
Map<String, dynamic> toJson() => {
'name': name,
if (location != null) 'location': location,
'is_default': isDefault,
};
}