feat(client): 登录/注册页照原型重建,ds 真相源组件族统一全部屏

- 登录/注册(login.html/register.html 1:1):两栏卡片+品牌渐变面板+主题小衣服
  pill(onSurface 变体)+记住我(记录并预填最近账号)+原型式 toast 校验;
  登录 fidelity 1.4–2.1% 三主题全绿;注册暂不入闸(少 门店编号/兑换券 字段,
  已记 CONTRACT,screens.mjs 留存根)
- ds 原子补齐:DsToast(.toast 单例)/DsCheck(.check/.agree)/DsButton lg 档/
  DsSelect 替换全部旧 DropdownButton/DsField label 在上/DsInput 后缀与密码形态
- 全屏统一:对话框按钮全 DsButton、盒式输入主题钉死(visualDensity.standard、
  h38、InputDecorationTheme 渗漏修复)、图标全 lucide、JetBrains Mono 三端同源、
  BrandMark 真相源 logo、只读模式写操作全量守卫(WriteGuard+DsToast)
- 出入库列表:版式对齐原型(卡片对齐+搜索框居中)、KPI 近30天滚动、结清后
  失效财务应收应付表;商品编辑抽屉介绍库改搜索下拉、图片双击全屏预览
- 删除旧 UI 死代码:DataTableCard/FormDialog/PageScaffold/SearchChip/
  SelectProductDialog/tabStateProvider
- golden/fidelity:stock-in/out 补注册(9%)、login 入册(8%)、goldens 全量重打;
  修复 pubspec flutter_web_plugins 非法声明(CI pub get 阻断)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JJ1g8XV1YhhmHRzhwWEW7o
This commit is contained in:
wangjia
2026-07-03 09:58:14 +08:00
parent ca7595b113
commit 6238b86dcb
286 changed files with 16831 additions and 11136 deletions
+59 -4
View File
@@ -40,6 +40,17 @@ class FinanceRecord {
}
}
/// 单据抬头(近期单据列表用):收付款显示类型,应收/应付回溯到来源单据
String get docTitle {
final ref = switch (refType) {
'stock_in' => '入库单',
'stock_out' => '出库单',
_ => null,
};
if (ref != null) return ref;
return typeLabel;
}
factory FinanceRecord.fromJson(Map<String, dynamic> json) => FinanceRecord(
id: (json['id'] as num).toInt(),
type: json['type'] as String,
@@ -47,13 +58,57 @@ class FinanceRecord {
balance: (json['balance'] as num).toDouble(),
status: json['status'] as String? ?? 'open',
refType: json['ref_type'] as String?,
refId:
json['ref_id'] != null ? (json['ref_id'] as num).toInt() : null,
refId: json['ref_id'] != null ? (json['ref_id'] as num).toInt() : null,
partnerName:
(json['partner'] as Map<String, dynamic>?)?['name'] as String?,
partnerId:
json['partner_id'] != null ? (json['partner_id'] as num).toInt() : null,
partnerId: json['partner_id'] != null
? (json['partner_id'] as num).toInt()
: null,
recordDate: json['record_date'] as String?,
remark: json['remark'] as String?,
);
}
/// GET /finance/summary 行:按往来单位分组的未结清应收/应付聚合
class PartnerFinanceSummary {
final int? partnerId;
final String partnerName;
final String type; // payable | receivable
final int recordCount;
final double totalAmount;
const PartnerFinanceSummary({
this.partnerId,
this.partnerName = '',
required this.type,
this.recordCount = 0,
required this.totalAmount,
});
factory PartnerFinanceSummary.fromJson(Map<String, dynamic> json) =>
PartnerFinanceSummary(
partnerId: json['partner_id'] != null
? (json['partner_id'] as num).toInt()
: null,
partnerName: json['partner_name'] as String? ?? '',
type: json['type'] as String,
recordCount: (json['record_count'] as num?)?.toInt() ?? 0,
totalAmount: (json['total_amount'] as num?)?.toDouble() ?? 0,
);
}
/// GET /finance/trend 点:月度收支(in=销售额、out=采购额,单位元)。
class TrendPoint {
final String month; // "2026-01"
final double inAmount;
final double outAmount;
const TrendPoint(
{required this.month, required this.inAmount, required this.outAmount});
factory TrendPoint.fromJson(Map<String, dynamic> json) => TrendPoint(
month: json['month'] as String? ?? '',
inAmount: (json['in'] as num?)?.toDouble() ?? 0,
outAmount: (json['out'] as num?)?.toDouble() ?? 0,
);
}
+11 -2
View File
@@ -79,12 +79,19 @@ class InventorySummary {
final double inStockQty;
final int shortageCount;
final int warningCount;
// 月初快照(KPI 环比):由 inventory_logs 反推
final int lastMonthSku;
final double lastMonthQty;
final double lastMonthValue;
const InventorySummary({
this.skuCount = 0,
this.stockValue = 0,
this.inStockQty = 0,
this.shortageCount = 0,
this.warningCount = 0,
this.lastMonthSku = 0,
this.lastMonthQty = 0,
this.lastMonthValue = 0,
});
factory InventorySummary.fromJson(Map<String, dynamic> j) => InventorySummary(
skuCount: (j['sku_count'] as num?)?.toInt() ?? 0,
@@ -92,6 +99,9 @@ class InventorySummary {
inStockQty: (j['in_stock_qty'] as num?)?.toDouble() ?? 0,
shortageCount: (j['shortage_count'] as num?)?.toInt() ?? 0,
warningCount: (j['warning_count'] as num?)?.toInt() ?? 0,
lastMonthSku: (j['last_month_sku'] as num?)?.toInt() ?? 0,
lastMonthQty: (j['last_month_qty'] as num?)?.toDouble() ?? 0,
lastMonthValue: (j['last_month_value'] as num?)?.toDouble() ?? 0,
);
}
@@ -138,8 +148,7 @@ class InventoryLog {
? (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,
refId: json['ref_id'] != null ? (json['ref_id'] as num).toInt() : null,
createdAt: json['created_at'] as String?,
);
}
+5 -5
View File
@@ -22,8 +22,8 @@ class LicenseInfo {
id: (json['id'] as num?)?.toInt() ?? 0,
type: json['type'] as String? ?? 'trial',
isActive: json['is_active'] as bool? ?? false,
maxDevices:
(json['max_devices'] as num?)?.toInt() ?? AppConstants.defaultMaxDevices,
maxDevices: (json['max_devices'] as num?)?.toInt() ??
AppConstants.defaultMaxDevices,
expiresAt: json['expires_at'] != null
? DateTime.tryParse(json['expires_at'] as String)
: null,
@@ -44,8 +44,7 @@ class LicenseInfo {
}
}
bool get isExpired =>
expiresAt != null && DateTime.now().isAfter(expiresAt!);
bool get isExpired => expiresAt != null && DateTime.now().isAfter(expiresAt!);
int? get daysRemaining {
if (expiresAt == null) return null;
@@ -62,5 +61,6 @@ class LicenseInfo {
bool get isReadOnlyPhase => phase == 'readonly' || phase == 'locked';
bool get isLockedPhase => phase == 'locked';
bool get needsAttention => phase == 'grace' || phase == 'readonly' || phase == 'locked';
bool get needsAttention =>
phase == 'grace' || phase == 'readonly' || phase == 'locked';
}
+2 -1
View File
@@ -30,7 +30,8 @@ class NumberRule {
String get exampleNo {
final now = DateTime.now();
final date = '${now.year}${now.month.toString().padLeft(2, '0')}${now.day.toString().padLeft(2, '0')}';
final date =
'${now.year}${now.month.toString().padLeft(2, '0')}${now.day.toString().padLeft(2, '0')}';
return '$prefix$date${(currentNo + 1).toString().padLeft(3, '0')}';
}
+12 -1
View File
@@ -2,11 +2,12 @@ class Partner {
final int id;
final String? code;
final String name;
final String type; // supplier | customer
final String type; // supplier | customer | supplier,customer(两者)
final String? contact;
final String? phone;
final String? address;
final String? bankAccount;
final double balance; // 期初余额(存档字段,不参与应收应付聚合)
final String status;
final String? remark;
@@ -19,10 +20,19 @@ class Partner {
this.phone,
this.address,
this.bankAccount,
this.balance = 0,
this.status = 'enabled',
this.remark,
});
/// 类型徽章文案(原型 b-供应商/b-客户/b-两者):SET 双值 = 两者
String get typeLabel {
final isSupplier = type.contains('supplier');
final isCustomer = type.contains('customer');
if (isSupplier && isCustomer) return '两者';
return isCustomer ? '客户' : '供应商';
}
factory Partner.fromJson(Map<String, dynamic> json) => Partner(
id: (json['id'] as num).toInt(),
code: json['code'] as String?,
@@ -32,6 +42,7 @@ class Partner {
phone: json['phone'] as String?,
address: json['address'] as String?,
bankAccount: json['bank_account'] as String?,
balance: (json['balance'] as num?)?.toDouble() ?? 0,
status: json['status'] as String? ?? 'enabled',
remark: json['remark'] as String?,
);
+12 -6
View File
@@ -82,12 +82,18 @@ class Product {
?.map((e) => ProductImage.fromJson(e as Map<String, dynamic>))
.toList() ??
[],
originName: (json['origin'] as Map<String, dynamic>?)?['name'] as String?,
shelfLifeName: (json['shelf_life'] as Map<String, dynamic>?)?['name'] as String?,
storageName: (json['storage'] as Map<String, dynamic>?)?['name'] as String?,
categoryName: (json['category'] as Map<String, dynamic>?)?['name'] as String?,
descriptionDocTitle: (json['description_doc'] as Map<String, dynamic>?)?['title'] as String?,
descriptionDocContent: (json['description_doc'] as Map<String, dynamic>?)?['content'] as String?,
originName:
(json['origin'] as Map<String, dynamic>?)?['name'] as String?,
shelfLifeName:
(json['shelf_life'] as Map<String, dynamic>?)?['name'] as String?,
storageName:
(json['storage'] as Map<String, dynamic>?)?['name'] as String?,
categoryName:
(json['category'] as Map<String, dynamic>?)?['name'] as String?,
descriptionDocTitle: (json['description_doc']
as Map<String, dynamic>?)?['title'] as String?,
descriptionDocContent: (json['description_doc']
as Map<String, dynamic>?)?['content'] as String?,
);
Map<String, dynamic> toJson() => {
+33 -29
View File
@@ -52,29 +52,30 @@ class StockInItem {
}
return StockInItem(
id: json['id'] != null ? (json['id'] as num).toInt() : null,
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(),
returnedQuantity: (json['returned_quantity'] as num?)?.toDouble() ?? 0,
batchNo: json['batch_no'] as String?,
productionDate: json['production_date'] as String?,
productName: lineOrProduct('product_name', 'name'),
productCode: lineOrProduct('product_code', 'code'),
productSeries: lineOrProduct('series', 'series'),
productSpec: lineOrProduct('spec', 'spec'),
productUnit: (json['product'] as Map<String, dynamic>?)?['unit'] as String?,
productOrigin: ((json['product'] as Map<String, dynamic>?)?['origin']
as Map<String, dynamic>?)?['name'] as String?,
productShelfLife: ((json['product'] as Map<String, dynamic>?)?['shelf_life']
as Map<String, dynamic>?)?['name'] as String?,
productStorage: ((json['product'] as Map<String, dynamic>?)?['storage']
as Map<String, dynamic>?)?['name'] as String?,
);
id: json['id'] != null ? (json['id'] as num).toInt() : null,
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(),
returnedQuantity: (json['returned_quantity'] as num?)?.toDouble() ?? 0,
batchNo: json['batch_no'] as String?,
productionDate: json['production_date'] as String?,
productName: lineOrProduct('product_name', 'name'),
productCode: lineOrProduct('product_code', 'code'),
productSeries: lineOrProduct('series', 'series'),
productSpec: lineOrProduct('spec', 'spec'),
productUnit:
(json['product'] as Map<String, dynamic>?)?['unit'] as String?,
productOrigin: ((json['product'] as Map<String, dynamic>?)?['origin']
as Map<String, dynamic>?)?['name'] as String?,
productShelfLife:
((json['product'] as Map<String, dynamic>?)?['shelf_life']
as Map<String, dynamic>?)?['name'] as String?,
productStorage: ((json['product'] as Map<String, dynamic>?)?['storage']
as Map<String, dynamic>?)?['name'] as String?,
);
}
Map<String, dynamic> toJson() => {
@@ -133,19 +134,23 @@ class StockInOrder {
orderNo: json['order_no'] as String,
type: json['type'] as String?,
warehouseId: (json['warehouse_id'] as num).toInt(),
warehouseName: (json['warehouse'] as Map<String, dynamic>?)?['name'] as String?,
warehouseName:
(json['warehouse'] as Map<String, dynamic>?)?['name'] as String?,
partnerId: json['partner_id'] != null
? (json['partner_id'] as num).toInt()
: null,
partnerName: (json['partner'] as Map<String, dynamic>?)?['name'] as String?,
partnerName:
(json['partner'] as Map<String, dynamic>?)?['name'] as String?,
operatorId: json['operator_id'] != null
? (json['operator_id'] as num).toInt()
: null,
operatorName: (json['operator'] as Map<String, dynamic>?)?['real_name'] as String?,
operatorName: (json['operator'] as Map<String, dynamic>?)?['real_name']
as String?,
reviewerId: json['reviewer_id'] != null
? (json['reviewer_id'] as num).toInt()
: null,
reviewerName: (json['reviewer'] as Map<String, dynamic>?)?['real_name'] as String?,
reviewerName: (json['reviewer'] as Map<String, dynamic>?)?['real_name']
as String?,
status: json['status'] as String,
returnState: json['return_state'] as String? ?? 'none',
orderDate: json['order_date'] as String?,
@@ -156,8 +161,7 @@ class StockInOrder {
remark: json['remark'] as String?,
items: json['items'] != null
? (json['items'] as List)
.map((e) =>
StockInItem.fromJson(e as Map<String, dynamic>))
.map((e) => StockInItem.fromJson(e as Map<String, dynamic>))
.toList()
: [],
);
+28 -25
View File
@@ -46,25 +46,25 @@ class StockOutItem {
}
return StockOutItem(
id: json['id'] != null ? (json['id'] as num).toInt() : null,
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(),
salePrice: (json['sale_price'] as num?)?.toDouble() ?? 0,
totalPrice: (json['total_price'] as num).toDouble(),
returnedQuantity: (json['returned_quantity'] as num?)?.toDouble() ?? 0,
productName: lineOrProduct('product_name', 'name'),
productCode: lineOrProduct('product_code', 'code'),
productSeries: lineOrProduct('series', 'series'),
productSpec: lineOrProduct('spec', 'spec'),
productUnit: (json['product'] as Map<String, dynamic>?)?['unit'] as String?,
// 批次号/生产日期:优先明细行快照,回退 product 关联(App 新建单据快照列常为空)
batchNo: lineOrProduct('batch_no', 'batch_no'),
productionDate: lineOrProduct('production_date', 'production_date'),
);
id: json['id'] != null ? (json['id'] as num).toInt() : null,
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(),
salePrice: (json['sale_price'] as num?)?.toDouble() ?? 0,
totalPrice: (json['total_price'] as num).toDouble(),
returnedQuantity: (json['returned_quantity'] as num?)?.toDouble() ?? 0,
productName: lineOrProduct('product_name', 'name'),
productCode: lineOrProduct('product_code', 'code'),
productSeries: lineOrProduct('series', 'series'),
productSpec: lineOrProduct('spec', 'spec'),
productUnit:
(json['product'] as Map<String, dynamic>?)?['unit'] as String?,
// 批次号/生产日期:优先明细行快照,回退 product 关联(App 新建单据快照列常为空)
batchNo: lineOrProduct('batch_no', 'batch_no'),
productionDate: lineOrProduct('production_date', 'production_date'),
);
}
}
@@ -116,19 +116,23 @@ class StockOutOrder {
orderNo: json['order_no'] as String,
type: json['type'] as String?,
warehouseId: (json['warehouse_id'] as num).toInt(),
warehouseName: (json['warehouse'] as Map<String, dynamic>?)?['name'] as String?,
warehouseName:
(json['warehouse'] as Map<String, dynamic>?)?['name'] as String?,
partnerId: json['partner_id'] != null
? (json['partner_id'] as num).toInt()
: null,
partnerName: (json['partner'] as Map<String, dynamic>?)?['name'] as String?,
partnerName:
(json['partner'] as Map<String, dynamic>?)?['name'] as String?,
operatorId: json['operator_id'] != null
? (json['operator_id'] as num).toInt()
: null,
operatorName: (json['operator'] as Map<String, dynamic>?)?['real_name'] as String?,
operatorName: (json['operator'] as Map<String, dynamic>?)?['real_name']
as String?,
reviewerId: json['reviewer_id'] != null
? (json['reviewer_id'] as num).toInt()
: null,
reviewerName: (json['reviewer'] as Map<String, dynamic>?)?['real_name'] as String?,
reviewerName: (json['reviewer'] as Map<String, dynamic>?)?['real_name']
as String?,
status: json['status'] as String,
returnState: json['return_state'] as String? ?? 'none',
orderDate: json['order_date'] as String?,
@@ -140,8 +144,7 @@ class StockOutOrder {
remark: json['remark'] as String?,
items: json['items'] != null
? (json['items'] as List)
.map((e) =>
StockOutItem.fromJson(e as Map<String, dynamic>))
.map((e) => StockOutItem.fromJson(e as Map<String, dynamic>))
.toList()
: [],
);
+6 -4
View File
@@ -24,10 +24,12 @@ class StockSummary {
);
/// 笔数环比(%);上月为 0 时返回 null(无从比较)。
double? get countDeltaPct =>
lastMonthCount == 0 ? null : (monthCount - lastMonthCount) / lastMonthCount * 100;
double? get countDeltaPct => lastMonthCount == 0
? null
: (monthCount - lastMonthCount) / lastMonthCount * 100;
/// 金额环比(%);上月为 0 时返回 null。
double? get amountDeltaPct =>
lastMonthAmount == 0 ? null : (monthAmount - lastMonthAmount) / lastMonthAmount * 100;
double? get amountDeltaPct => lastMonthAmount == 0
? null
: (monthAmount - lastMonthAmount) / lastMonthAmount * 100;
}
+4 -1
View File
@@ -3,9 +3,10 @@ class AppUser {
final String username;
final String? realName;
final String? phone;
final String role; // admin | operator | readonly
final String role; // superadmin | admin | operator | readonly
final bool isActive;
final String? createdAt;
final String? lastLoginAt;
const AppUser({
required this.id,
@@ -15,6 +16,7 @@ class AppUser {
required this.role,
required this.isActive,
this.createdAt,
this.lastLoginAt,
});
String get roleLabel {
@@ -40,5 +42,6 @@ class AppUser {
role: json['role'] as String? ?? 'operator',
isActive: json['is_active'] as bool? ?? true,
createdAt: json['created_at'] as String?,
lastLoginAt: json['last_login_at'] as String?,
);
}