feat(client): 库存/盘点/财务窄屏对齐移动原型(KPI 网格/卡片流/图标徽章/sheet)
- 库存列表:窄屏隐藏页内大标题头;KPI 2×2 MKpiGrid(缺货卡 warn+点击筛选); MSearchRow(详搜钮开筛选 sheet:状态/规格/系列);卡片徽章换图标变体 - 库存盘点:窄屏卡片流(单号/范围·项数/进行中徽章带图标/差异/日期)+ 详情 sheet(drow 键值行)+ 底部操作条;DateTime.now→appNow 可注入时钟 - 财务:窄屏隐藏大标题头;KPI 2×2(收入 up 绿/支出 down 红/应收 warn); DsSeg 应收/应付/流水三分段 + 卡片流(类型徽章带图标、金额正负色) - golden:新增 m_inventory / m_inventory_check / m_finance 三屏×三主题 (390×844@2x);重生成既有 inventory_list_mobile / finance_mobile 基准 (桌面 golden 零改动) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -22,6 +22,8 @@ import '../../widgets/ds/ds_atoms.dart';
|
||||
import '../../widgets/ds/ds_bar_chart.dart';
|
||||
import '../../widgets/ds/ds_kpi.dart';
|
||||
import '../../widgets/ds/ds_table.dart';
|
||||
import '../../widgets/ds/m_kpi_grid.dart';
|
||||
import '../../widgets/ds/status_icon_map.dart';
|
||||
import '../../widgets/finance_entry_dialog.dart';
|
||||
import '../../widgets/finance_partner_drawer.dart';
|
||||
import '../../widgets/mobile_list_card.dart';
|
||||
@@ -42,6 +44,8 @@ class _FinanceScreenState extends ConsumerState<FinanceScreen> {
|
||||
String _rangeLabel = '本月';
|
||||
// 流水类型筛选(原型 3 chips 扩到 5)
|
||||
String _flowChip = '全部';
|
||||
// 窄屏三分段(原型 m-finance .seg:应收 / 应付 / 流水)
|
||||
int _mSeg = 0;
|
||||
|
||||
static const _flowChips = ['全部', '应收', '应付', '收款', '付款'];
|
||||
static const _chipToType = {
|
||||
@@ -139,6 +143,40 @@ class _FinanceScreenState extends ConsumerState<FinanceScreen> {
|
||||
final flowsAsync = ref.watch(financeListProvider);
|
||||
final flows = flowsAsync.valueOrNull?.data ?? const <FinanceRecord>[];
|
||||
|
||||
// 窄屏(原型 m-finance):隐藏页内大标题头,KPI 2×2 网格 + 三分段 + 卡片流。
|
||||
if (mobile) {
|
||||
final content = Container(
|
||||
color: t.bg,
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(AppDims.sp4),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
_timeRow(t, true),
|
||||
_mKpis(t),
|
||||
const SizedBox(height: 14),
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: DsSeg(
|
||||
items: const ['应收', '应付', '流水'],
|
||||
index: _mSeg,
|
||||
onChanged: (i) => setState(() => _mSeg = i)),
|
||||
),
|
||||
),
|
||||
..._mSegBody(t, flows),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
return Stack(children: [
|
||||
content,
|
||||
if (flowsAsync.isLoading)
|
||||
const Positioned.fill(child: DsLoadingScrim()),
|
||||
]);
|
||||
}
|
||||
|
||||
final content = Container(
|
||||
color: t.bg,
|
||||
child: SingleChildScrollView(
|
||||
@@ -645,4 +683,184 @@ class _FinanceScreenState extends ConsumerState<FinanceScreen> {
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
// ── 窄屏形态(原型 m-finance)──────────────────────────────────────
|
||||
|
||||
/// delta 箭头/色调映射(与 DsKpi 同规则:▲/▼ 文本字符)。
|
||||
String _mDeltaText(String text, DsKpiDelta tone) => switch (tone) {
|
||||
DsKpiDelta.up => '▲ $text',
|
||||
DsKpiDelta.down => '▼ $text',
|
||||
DsKpiDelta.neutral => text,
|
||||
};
|
||||
|
||||
MKpiDeltaTone _mTone(DsKpiDelta tone) => switch (tone) {
|
||||
DsKpiDelta.up => MKpiDeltaTone.up,
|
||||
DsKpiDelta.down => MKpiDeltaTone.down,
|
||||
DsKpiDelta.neutral => MKpiDeltaTone.normal,
|
||||
};
|
||||
|
||||
/// 窄屏 KPI 2×2(原型 .m-kpi:本月收入 / 本月支出 / 应收合计 / 应付合计)。
|
||||
Widget _mKpis(dynamic t) {
|
||||
final outSum = ref.watch(stockOutSummaryProvider).valueOrNull;
|
||||
final inSum = ref.watch(stockInSummaryProvider).valueOrNull;
|
||||
final rows = ref.watch(financePartnerRowsProvider).valueOrNull ??
|
||||
const <PartnerFinanceRow>[];
|
||||
var recv = 0.0, pay = 0.0, openCount = 0;
|
||||
for (final r in rows) {
|
||||
recv += r.recv;
|
||||
pay += r.pay;
|
||||
openCount += r.openCount;
|
||||
}
|
||||
final (saleDelta, saleTone) = outSum != null
|
||||
? _momDelta(outSum.monthAmount, outSum.lastMonthAmount)
|
||||
: ('较上月 —', DsKpiDelta.neutral);
|
||||
final (buyDelta, buyTone) = inSum != null
|
||||
? _momDelta(inSum.monthAmount, inSum.lastMonthAmount)
|
||||
: ('较上月 —', DsKpiDelta.neutral);
|
||||
return MKpiGrid(items: [
|
||||
MKpiItem(
|
||||
label: '本月收入',
|
||||
value: outSum != null ? _yuanWan(outSum.monthAmount) : '—',
|
||||
delta: _mDeltaText(saleDelta, saleTone),
|
||||
deltaTone: _mTone(saleTone),
|
||||
),
|
||||
MKpiItem(
|
||||
label: '本月支出',
|
||||
value: inSum != null ? _yuanWan(inSum.monthAmount) : '—',
|
||||
delta: _mDeltaText(buyDelta, buyTone),
|
||||
deltaTone: _mTone(buyTone),
|
||||
),
|
||||
// 原型 delta「12 笔未结」:应收/应付分侧笔数无数据源 → 用未结清总笔数(已知差异)
|
||||
MKpiItem(
|
||||
label: '应收合计',
|
||||
value: _yuanWan(recv),
|
||||
delta: '未结清 $openCount 笔',
|
||||
deltaTone: MKpiDeltaTone.warn,
|
||||
),
|
||||
MKpiItem(
|
||||
label: '应付合计',
|
||||
value: _yuanWan(pay),
|
||||
delta: '按到期及时结清',
|
||||
deltaTone: MKpiDeltaTone.normal,
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
/// 三分段列表体:m-section 计数标题 + 卡片流(应收/应付=往来汇总,流水=收支记录)。
|
||||
List<Widget> _mSegBody(dynamic t, List<FinanceRecord> flows) {
|
||||
Widget section(String title, int count) => Padding(
|
||||
padding: const EdgeInsets.fromLTRB(2, 16, 2, 8),
|
||||
child: Text('$title · 共 $count',
|
||||
style: TextStyle(
|
||||
fontSize: AppDims.fsSm,
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: .4,
|
||||
color: t.muted)),
|
||||
);
|
||||
Widget empty() => Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Center(
|
||||
child: Text('暂无数据',
|
||||
style: TextStyle(color: t.muted, fontSize: AppDims.fsSm))),
|
||||
);
|
||||
List<Widget> stacked(List<Widget> cards) => [
|
||||
for (var i = 0; i < cards.length; i++) ...[
|
||||
if (i > 0) const SizedBox(height: 10),
|
||||
cards[i],
|
||||
],
|
||||
];
|
||||
|
||||
if (_mSeg == 2) {
|
||||
return [
|
||||
section('资金流水', flows.length),
|
||||
if (flows.isEmpty)
|
||||
empty()
|
||||
else
|
||||
...stacked([for (final r in flows) _mFlowCard(t, r)]),
|
||||
];
|
||||
}
|
||||
final async = ref.watch(financePartnerRowsProvider);
|
||||
return async.when(
|
||||
loading: () => [
|
||||
const Padding(
|
||||
padding: EdgeInsets.all(24),
|
||||
child: Center(child: CircularProgressIndicator())),
|
||||
],
|
||||
error: (e, _) => [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Center(
|
||||
child: Text('汇总加载失败',
|
||||
style: TextStyle(color: t.muted, fontSize: AppDims.fsSm))),
|
||||
),
|
||||
],
|
||||
data: (rows) {
|
||||
final ar = _mSeg == 0;
|
||||
final list = rows.where((r) => ar ? r.recv > 0 : r.pay > 0).toList();
|
||||
return [
|
||||
section(ar ? '应收账款' : '应付账款', list.length),
|
||||
if (list.isEmpty)
|
||||
empty()
|
||||
else
|
||||
...stacked(
|
||||
[for (final r in list) _mPartnerCard(t, r, ar: ar)]),
|
||||
];
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// 应收/应付卡(原型 .m-card:往来单位 + 金额 + 徽章带图标)。
|
||||
Widget _mPartnerCard(dynamic t, PartnerFinanceRow r, {required bool ar}) {
|
||||
return MobileListCard(
|
||||
onTap: () => showFinancePartnerDrawer(context, row: r),
|
||||
title: Text(r.name),
|
||||
subtitle: Text('未结清 ${r.openCount} 笔'),
|
||||
trailing: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
DsBadge('未结清',
|
||||
tone: DsBadgeTone.warn, icon: statusIcon('未结清')),
|
||||
const SizedBox(height: 6),
|
||||
Text(_yuan(ar ? r.recv : r.pay),
|
||||
style: TextStyle(
|
||||
fontFamily: AppFonts.mono,
|
||||
fontFamilyFallback: AppFonts.monoFallback,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 13,
|
||||
color: t.heading)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 流水卡(原型 .m-card:类型徽章带图标 收款/付款 + 金额正负色)。
|
||||
Widget _mFlowCard(dynamic t, FinanceRecord r) {
|
||||
final isIn = r.type == 'receipt' || r.type == 'receivable';
|
||||
return MobileListCard(
|
||||
title: Text(
|
||||
(r.partnerName?.isNotEmpty == true) ? r.partnerName! : r.typeLabel),
|
||||
subtitle: Text((r.recordDate ?? '').split('T').first),
|
||||
trailing: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
DsBadge(r.typeLabel,
|
||||
tone: switch (r.type) {
|
||||
'receipt' => DsBadgeTone.ok,
|
||||
'payment' => DsBadgeTone.danger,
|
||||
'receivable' => DsBadgeTone.warn,
|
||||
_ => DsBadgeTone.info,
|
||||
},
|
||||
icon: statusIcon(r.typeLabel)),
|
||||
const SizedBox(height: 6),
|
||||
Text('${isIn ? '+' : '-'}${_yuan(r.amount.abs())}',
|
||||
style: TextStyle(
|
||||
fontFamily: AppFonts.mono,
|
||||
fontFamilyFallback: AppFonts.monoFallback,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 13,
|
||||
color: isIn ? t.success : t.danger)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user