feat(client): 库存卡片显示单价(进价)+ 成本按角色隐藏

- 手机库存卡副行追加进价(编号 · 规格 · ¥xxx),仅管理员可见
- 宽屏成本价/总价列、货值 KPI、商品抽屉成本行对非管理员隐藏(isAdminProvider)
- m-inventory 原型同步 + 移动 golden ×6 重录(管理员形态基准)

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-06 08:11:15 +08:00
parent 75d0accb14
commit d9f1f6956e
11 changed files with 30 additions and 10 deletions
@@ -57,7 +57,7 @@ function render(){
if(!rows.length){ L.innerHTML=`<div class="m-empty"><svg viewBox="0 0 24 24"><use href="#i-box"/></svg>没有匹配的商品 · 调整筛选或搜索</div>`; return; }
L.innerHTML=rows.map((it,i)=>`<a class="m-card" onclick="openItem(${ITEMS.indexOf(it)})">
<div class="m-row">
<div class="mc-main"><div class="mc-nm">${it.name}</div><div class="mc-sub">${it.code} · ${it.spec}</div></div>
<div class="mc-main"><div class="mc-nm">${it.name}</div><div class="mc-sub">${it.code} · ${it.spec} · ${it.cost}</div></div><!-- 单价=成本进价,仅管理员可见(2026-07-06 -->
<div class="mc-right"><span class="badge b-${it.status}">${it.status}</span><span class="mc-amt"><span class="${it.qty<=10?'qty-low':''}">${it.qty}</span> ${it.unit}</span></div>
<div class="mc-chev"><svg viewBox="0 0 24 24"><use href="#i-chevron-right"/></svg></div>
</div>
@@ -5,6 +5,7 @@ import 'package:lucide_icons_flutter/lucide_icons.dart';
import '../../core/responsive/responsive.dart';
import '../../core/theme/context_tokens.dart';
import '../../core/theme/app_dims.g.dart';
import '../../core/auth/auth_state.dart';
import '../../core/storage/column_prefs.dart';
import '../../core/utils/export_util.dart';
import '../../models/inventory.dart';
@@ -114,12 +115,17 @@ class _InventoryListScreenState extends ConsumerState<InventoryListScreen> {
);
}
// 成本仅管理员可见(与出库同口径):非管理员隐藏成本价/总价列、卡片单价、货值 KPI。
bool get _isAdmin => ref.watch(isAdminProvider);
bool _colAllowed(ColDef c) =>
_isAdmin || (c.key != 'cost' && c.key != 'price');
// ── 列设置(原型 colBtn → openColMenu:多选保持打开) ──
void _openColMenu(BuildContext anchorContext) {
showDsMultiMenu<String>(
anchorContext,
itemsBuilder: () => [
for (final c in _colDefs.where((c) => !c.required))
for (final c in _colDefs.where((c) => !c.required && _colAllowed(c)))
DsMenuItem(
value: c.key,
label: c.label,
@@ -528,6 +534,8 @@ class _InventoryListScreenState extends ConsumerState<InventoryListScreen> {
final sub = [
if (item.productCode.isNotEmpty) item.productCode,
if (item.spec.isNotEmpty) item.spec,
// 单价(成本进价)仅管理员可见(2026-07-06 用户拍板)
if (_isAdmin && item.unitPrice != null) _fmtCost(item.unitPrice!),
].join(' · ');
return MCard(
onTap: item.productId != null ? () => _openEditor(item) : null,
@@ -626,8 +634,9 @@ class _InventoryListScreenState extends ConsumerState<InventoryListScreen> {
.toList();
final hidden = _hiddenCols ?? <String>{};
final visibleCols =
_colDefs.where((c) => !hidden.contains(c.key)).toList();
final visibleCols = _colDefs
.where((c) => !hidden.contains(c.key) && _colAllowed(c))
.toList();
final mobile = context.isMobile;
// 原型 .main{padding:22px 26px}:桌面整页留白统一在此;窄屏保持紧凑。
@@ -715,7 +724,8 @@ class _InventoryListScreenState extends ConsumerState<InventoryListScreen> {
),
MKpiItem(
label: '库存货值',
value: summary != null
// 货值=成本口径,非管理员后端抹零,前端显示占位
value: _isAdmin && summary != null
? yuanWan(summary.stockValue)
: '',
delta: _mDeltaText(valDelta, valTone),
@@ -752,8 +762,9 @@ class _InventoryListScreenState extends ConsumerState<InventoryListScreen> {
),
DsKpi(
title: '库存货值',
value:
summary != null ? yuanWan(summary.stockValue) : '',
value: _isAdmin && summary != null
? yuanWan(summary.stockValue)
: '',
icon: LucideIcons.database,
tone: DsKpiTone.ok,
delta: valDelta,
@@ -11,6 +11,7 @@ import 'package:go_router/go_router.dart';
import 'package:image_picker/image_picker.dart';
import 'package:lucide_icons_flutter/lucide_icons.dart';
import '../core/auth/auth_state.dart';
import '../core/config/app_config.dart';
import '../core/utils/dialog_util.dart';
import '../core/responsive/responsive.dart';
@@ -356,10 +357,14 @@ class _ProductEditorDrawerState extends ConsumerState<_ProductEditorDrawer> {
b('${_fmtQty(widget.qty!)} ${widget.unit ?? p.unit}'.trim())
));
}
// 成本仅管理员可见(与库存列表/出库同口径);p.purchasePrice 兜底也要一并门控
final isAdmin = ref.watch(isAdminProvider);
final cost = widget.cost ?? p.purchasePrice;
if (cost != null && cost > 0) rows.add(('成本价(单瓶)', b(_fmtMoney(cost))));
if (widget.qty != null && cost != null && cost > 0) {
rows.add(('总成本价', b(_fmtMoney(widget.qty! * cost))));
if (isAdmin && cost != null && cost > 0) {
rows.add(('成本价(单瓶)', b(_fmtMoney(cost))));
if (widget.qty != null) {
rows.add(('总成本价', b(_fmtMoney(widget.qty! * cost))));
}
}
if ((widget.status ?? '').isNotEmpty) {
rows.add((
Binary file not shown.

Before

Width:  |  Height:  |  Size: 200 KiB

After

Width:  |  Height:  |  Size: 213 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 197 KiB

After

Width:  |  Height:  |  Size: 209 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 202 KiB

After

Width:  |  Height:  |  Size: 215 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 136 KiB

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 134 KiB

After

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 138 KiB

After

Width:  |  Height:  |  Size: 145 KiB

@@ -242,6 +242,8 @@ List<Override> _overrides() => [
productSeriesListProvider.overrideWith(() => _FakeSeriesNotifier()),
productSpecListProvider.overrideWith(() => _FakeSpecNotifier()),
isReadonlyProvider.overrideWithValue(false),
// 成本列/货值/单价仅管理员可见——golden 基准取管理员形态
isAdminProvider.overrideWithValue(true),
inventorySummaryProvider.overrideWith((ref) => const InventorySummary(
skuCount: 1284,
stockValue: 2640000,
@@ -165,6 +165,8 @@ List<Override> _overrides() => [
productSeriesListProvider.overrideWith(() => _FakeSeriesNotifier()),
productSpecListProvider.overrideWith(() => _FakeSpecNotifier()),
isReadonlyProvider.overrideWithValue(false),
// 成本列/货值/单价仅管理员可见——golden 基准取管理员形态(卡片含单价)
isAdminProvider.overrideWithValue(true),
inventorySummaryProvider.overrideWith((ref) => const InventorySummary(
skuCount: 1284,
stockValue: 2640000,