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:
@@ -0,0 +1,55 @@
|
||||
// widgets/ds/ds_switch.dart — 原型 atoms.css .switch 的 Flutter 对照(2026-07-07 登记)。
|
||||
// 38×22 pill 轨道 + 18px 圆点滑块:on=primary / off=border / disabled 45% 透明。
|
||||
// 用于布尔即时生效场景(库存⇄在售挂售切换);不同于 Material Switch 的尺寸与配色。
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../core/theme/context_tokens.dart';
|
||||
|
||||
class DsSwitch extends StatelessWidget {
|
||||
final bool value;
|
||||
final ValueChanged<bool>? onChanged; // null = disabled
|
||||
const DsSwitch({super.key, required this.value, this.onChanged});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final t = context.tokens;
|
||||
final enabled = onChanged != null;
|
||||
return Semantics(
|
||||
toggled: value,
|
||||
button: true,
|
||||
child: GestureDetector(
|
||||
onTap: enabled ? () => onChanged!(!value) : null,
|
||||
child: Opacity(
|
||||
opacity: enabled ? 1 : .45,
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 180),
|
||||
width: 38,
|
||||
height: 22,
|
||||
padding: const EdgeInsets.all(2),
|
||||
decoration: BoxDecoration(
|
||||
color: value ? t.primary : t.border,
|
||||
borderRadius: BorderRadius.circular(999),
|
||||
),
|
||||
child: AnimatedAlign(
|
||||
duration: const Duration(milliseconds: 180),
|
||||
curve: Curves.easeOut,
|
||||
alignment: value ? Alignment.centerRight : Alignment.centerLeft,
|
||||
child: Container(
|
||||
width: 18,
|
||||
height: 18,
|
||||
decoration: BoxDecoration(
|
||||
color: t.surface,
|
||||
shape: BoxShape.circle,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: t.shadow, blurRadius: 3, offset: const Offset(0, 1)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -5,8 +5,10 @@ import 'package:flutter/widgets.dart';
|
||||
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
||||
|
||||
const Map<String, IconData> kStatusIcons = {
|
||||
// 库存状态
|
||||
// 库存状态(三态落库 2026-07-07:在售/库存/已售;预警/缺货为数量派生提示)
|
||||
'在售': LucideIcons.shoppingCart, // i-cart
|
||||
'库存': LucideIcons.box, // i-box
|
||||
'已售': LucideIcons.check, // i-check
|
||||
'预警': LucideIcons.triangleAlert, // i-alert
|
||||
'缺货': LucideIcons.x, // i-close
|
||||
// 单据状态
|
||||
|
||||
@@ -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 / sold(switch 初始态)
|
||||
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;
|
||||
|
||||
// 库存三态挂售 switch(stock⇄on_sale;sold 只读无 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(
|
||||
|
||||
Reference in New Issue
Block a user