feat(client): 库存三态 UI——抽屉挂售 switch + 三态徽章/筛选/已售 KPI 卡

- 新组件 DsSwitch(原型 atoms .switch 对照,L1 已登记:38×22 pill,on=primary)
- 商品抽屉状态行:三态徽章后跟挂售 switch(在售⇄库存 即时生效,已售只读,
  readonly 禁用),桌面 drawer / 移动 sheet 同组件
- 列表状态列/卡片徽章三态化(在售绿/库存蓝/已售灰,icons BADGE_ICON 补两词);
  状态筛选改「全部/在售/库存/已售」走服务端;预警缺货退出状态维度由数量染色承担
- KPI 第四卡「缺货预警」→「已售」(sold_count,点击筛选已售)
- 原型同步:atoms/index 登记 switch 与 b-库存/b-已售、inventory 两屏三态数据、
  抽屉状态行 switch;12 道 ds 闸 + L1 同源闸全过
- golden 重录 ×9(三态徽章 + 已售卡形态)

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-07 13:35:19 +08:00
parent 73955a139c
commit 65c672b4ad
26 changed files with 264 additions and 56 deletions
+78 -10
View File
@@ -20,10 +20,12 @@ import '../core/theme/app_dims.g.dart';
import '../core/theme/context_tokens.dart';
import '../models/product.dart';
import '../models/product_image.dart';
import '../providers/inventory_provider.dart' show inventoryRepositoryProvider;
import '../providers/product_option_provider.dart';
import '../providers/product_provider.dart';
import '../providers/shop_provider.dart' show shopInfoProvider;
import 'ds/ds_atoms.dart';
import 'ds/ds_switch.dart';
import 'ds/grid_combo_cell.dart';
import 'ds/m_sheet.dart';
import 'searchable_option_field.dart';
@@ -41,7 +43,10 @@ Future<void> showProductEditorDrawer(
double? qty,
String? unit,
double? cost,
String? status, // 在售 / 预警 / 缺货
String? status, // 在售 / 库存 / 已售(三态显示标签)
int? inventoryId, // 库存行 id:非空时状态行显示挂售 switch
String? inventoryStatus, // stock / on_sale / soldswitch 初始态)
VoidCallback? onStatusChanged, // switch 切换成功后回调(列表刷新)
}) {
// 窄屏:底部 sheet 形态(原型 m-inventory openItem 内嵌公开页编辑)
if (context.isMobile) {
@@ -56,7 +61,10 @@ Future<void> showProductEditorDrawer(
qty: qty,
unit: unit,
cost: cost,
status: status),
status: status,
inventoryId: inventoryId,
inventoryStatus: inventoryStatus,
onStatusChanged: onStatusChanged),
),
);
}
@@ -83,7 +91,10 @@ Future<void> showProductEditorDrawer(
qty: qty,
unit: unit,
cost: cost,
status: status),
status: status,
inventoryId: inventoryId,
inventoryStatus: inventoryStatus,
onStatusChanged: onStatusChanged),
),
),
),
@@ -106,12 +117,18 @@ class _ProductEditorDrawer extends ConsumerStatefulWidget {
final String? unit;
final double? cost;
final String? status;
final int? inventoryId;
final String? inventoryStatus;
final VoidCallback? onStatusChanged;
const _ProductEditorDrawer({
required this.productId,
this.qty,
this.unit,
this.cost,
this.status,
this.inventoryId,
this.inventoryStatus,
this.onStatusChanged,
});
@override
@@ -125,6 +142,32 @@ class _ProductEditorDrawerState extends ConsumerState<_ProductEditorDrawer> {
bool _uploading = false;
bool _saving = false;
// 库存三态挂售 switchstock⇄on_salesold 只读无 switch
late String? _invStatus = widget.inventoryStatus;
bool _statusSaving = false;
Future<void> _toggleOnSale(bool on) async {
final id = widget.inventoryId;
if (id == null || _statusSaving) return;
final next = on ? 'on_sale' : 'stock';
final prev = _invStatus;
setState(() {
_invStatus = next;
_statusSaving = true;
});
try {
await ref.read(inventoryRepositoryProvider).updateStatus(id, next);
widget.onStatusChanged?.call();
} catch (e) {
if (mounted) {
setState(() => _invStatus = prev);
showDsToast(context, '状态更新失败:$e', bg: context.tokens.danger);
}
} finally {
if (mounted) setState(() => _statusSaving = false);
}
}
// 介绍选择态(.pe-pick / .pe-text
String? _introTitle; // 已选介绍库标题;null → 占位「从介绍库选择…」
int? _introDocId;
@@ -366,15 +409,40 @@ class _ProductEditorDrawerState extends ConsumerState<_ProductEditorDrawer> {
rows.add(('总成本价', b(_fmtMoney(widget.qty! * cost))));
}
}
if ((widget.status ?? '').isNotEmpty) {
// 状态行:三态徽章 + 挂售 switch(在售⇄库存 即时生效;已售只读;readonly 禁用)
final invStatus = _invStatus;
final statusLabel = switch (invStatus) {
'on_sale' => '在售',
'sold' => '已售',
'stock' => '库存',
_ => widget.status ?? '',
};
if (statusLabel.isNotEmpty) {
final badge = DsBadge(statusLabel,
tone: switch (statusLabel) {
'在售' => DsBadgeTone.ok,
'已售' => DsBadgeTone.muted,
'库存' => DsBadgeTone.info,
'缺货' => DsBadgeTone.danger,
'预警' => DsBadgeTone.warn,
_ => DsBadgeTone.ok,
});
final canToggle = widget.inventoryId != null &&
invStatus != null &&
invStatus != 'sold' &&
!ref.watch(isReadonlyProvider);
rows.add((
'状态',
DsBadge(widget.status!,
tone: switch (widget.status) {
'缺货' => DsBadgeTone.danger,
'预警' => DsBadgeTone.warn,
_ => DsBadgeTone.ok,
})
Row(mainAxisSize: MainAxisSize.min, children: [
badge,
if (widget.inventoryId != null && invStatus != null && invStatus != 'sold') ...[
const SizedBox(width: 10),
DsSwitch(
value: invStatus == 'on_sale',
onChanged: canToggle && !_statusSaving ? _toggleOnSale : null,
),
],
])
));
}
return Column(