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:
@@ -3,7 +3,10 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import '../../core/responsive/responsive.dart';
|
||||
import '../../core/theme/app_dims.g.dart';
|
||||
import '../../core/theme/context_tokens.dart';
|
||||
import '../../core/utils/clock.dart';
|
||||
import '../../models/inventory.dart';
|
||||
import '../../models/warehouse.dart';
|
||||
import '../../core/config/app_constants.dart';
|
||||
@@ -12,6 +15,9 @@ import '../../providers/warehouse_provider.dart';
|
||||
import '../../core/theme/app_fonts.dart';
|
||||
import '../../widgets/ds/ds_atoms.dart';
|
||||
import '../../widgets/ds/ds_toast.dart';
|
||||
import '../../widgets/ds/m_sheet.dart';
|
||||
import '../../widgets/ds/status_icon_map.dart';
|
||||
import '../../widgets/mobile_list_card.dart';
|
||||
|
||||
class InventoryCheckScreen extends ConsumerStatefulWidget {
|
||||
const InventoryCheckScreen({super.key});
|
||||
@@ -31,12 +37,18 @@ class _InventoryCheckScreenState extends ConsumerState<InventoryCheckScreen> {
|
||||
final List<_CheckItem> _checkItems = [];
|
||||
|
||||
String get _checkNo {
|
||||
final now = DateTime.now();
|
||||
final now = appNow();
|
||||
final date =
|
||||
'${now.year}${now.month.toString().padLeft(2, '0')}${now.day.toString().padLeft(2, '0')}';
|
||||
return 'PD$date${_selectedWarehouse?.id.toString().padLeft(3, '0') ?? '001'}';
|
||||
}
|
||||
|
||||
/// 盘点日期(appNow 可注入时钟,golden 冻结确定化)。
|
||||
String get _dateStr {
|
||||
final now = appNow();
|
||||
return '${now.year}-${now.month.toString().padLeft(2, '0')}-${now.day.toString().padLeft(2, '0')}';
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
for (final item in _checkItems) {
|
||||
@@ -81,9 +93,7 @@ class _InventoryCheckScreenState extends ConsumerState<InventoryCheckScreen> {
|
||||
}
|
||||
setState(() => _submitting = true);
|
||||
try {
|
||||
final now = DateTime.now();
|
||||
final dateStr =
|
||||
'${now.year}-${now.month.toString().padLeft(2, '0')}-${now.day.toString().padLeft(2, '0')}';
|
||||
final dateStr = _dateStr;
|
||||
|
||||
final items = _checkItems.map((item) {
|
||||
final actual =
|
||||
@@ -125,6 +135,10 @@ class _InventoryCheckScreenState extends ConsumerState<InventoryCheckScreen> {
|
||||
Widget build(BuildContext context) {
|
||||
final asyncWarehouses = ref.watch(warehouseListProvider);
|
||||
|
||||
// 窄屏(原型 m-inventory-check):壳顶栏带标题/返回 → 隐藏页内大标题头,
|
||||
// 盘点单以卡片流呈现(点卡开详情 sheet),底部操作条提交。
|
||||
if (context.isMobile) return _buildMobile(asyncWarehouses);
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: context.tokens.bg,
|
||||
body: Column(
|
||||
@@ -173,90 +187,7 @@ class _InventoryCheckScreenState extends ConsumerState<InventoryCheckScreen> {
|
||||
child: Column(
|
||||
children: [
|
||||
// Basic info
|
||||
Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('盘点基本信息',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: context.tokens.primaryDark)),
|
||||
const SizedBox(height: 16),
|
||||
Wrap(
|
||||
spacing: 16,
|
||||
runSpacing: 16,
|
||||
children: [
|
||||
_InfoField(
|
||||
label: '盘点单号',
|
||||
child: InputDecorator(
|
||||
decoration: const InputDecoration(),
|
||||
child: Text(
|
||||
_checkNo,
|
||||
style: const TextStyle(
|
||||
fontFamily: AppFonts.mono,
|
||||
fontFamilyFallback:
|
||||
AppFonts.monoFallback,
|
||||
fontSize: 13),
|
||||
),
|
||||
),
|
||||
),
|
||||
_InfoField(
|
||||
label: '盘点仓库',
|
||||
child: asyncWarehouses.when(
|
||||
loading: () =>
|
||||
const LinearProgressIndicator(),
|
||||
error: (e, _) => Text('$e',
|
||||
style: TextStyle(
|
||||
color: context.tokens.danger,
|
||||
fontSize: 12)),
|
||||
data: (warehouses) => DsSelect<Warehouse>(
|
||||
value: _selectedWarehouse,
|
||||
hint: '请选择仓库',
|
||||
options: [
|
||||
for (final w in warehouses) (w, w.name),
|
||||
],
|
||||
onChanged: (w) {
|
||||
setState(() => _selectedWarehouse = w);
|
||||
_loadInventory(w);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
_InfoField(
|
||||
label: '盘点类型',
|
||||
child: DsSelect<String>(
|
||||
value: _checkType,
|
||||
options: const [
|
||||
('全盘', '全盘'),
|
||||
('抽盘', '抽盘'),
|
||||
('循环盘点', '循环盘点'),
|
||||
],
|
||||
onChanged: (v) =>
|
||||
setState(() => _checkType = v),
|
||||
),
|
||||
),
|
||||
_InfoField(
|
||||
label: '盘点日期',
|
||||
child: InputDecorator(
|
||||
decoration: const InputDecoration(),
|
||||
child: Builder(builder: (ctx) {
|
||||
final now = DateTime.now();
|
||||
return Text(
|
||||
'${now.year}-${now.month.toString().padLeft(2, '0')}-${now.day.toString().padLeft(2, '0')}',
|
||||
style: const TextStyle(fontSize: 13),
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
_basicInfoCard(asyncWarehouses),
|
||||
const SizedBox(height: 12),
|
||||
// Check items table
|
||||
Card(
|
||||
@@ -408,6 +339,256 @@ class _InventoryCheckScreenState extends ConsumerState<InventoryCheckScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
/// 盘点基本信息卡(桌面/移动共用):单号 / 仓库 / 类型 / 日期。
|
||||
Widget _basicInfoCard(AsyncValue<List<Warehouse>> asyncWarehouses) {
|
||||
return Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('盘点基本信息',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: context.tokens.primaryDark)),
|
||||
const SizedBox(height: 16),
|
||||
Wrap(
|
||||
spacing: 16,
|
||||
runSpacing: 16,
|
||||
children: [
|
||||
_InfoField(
|
||||
label: '盘点单号',
|
||||
child: InputDecorator(
|
||||
decoration: const InputDecoration(),
|
||||
child: Text(
|
||||
_checkNo,
|
||||
style: const TextStyle(
|
||||
fontFamily: AppFonts.mono,
|
||||
fontFamilyFallback: AppFonts.monoFallback,
|
||||
fontSize: 13),
|
||||
),
|
||||
),
|
||||
),
|
||||
_InfoField(
|
||||
label: '盘点仓库',
|
||||
child: asyncWarehouses.when(
|
||||
loading: () => const LinearProgressIndicator(),
|
||||
error: (e, _) => Text('$e',
|
||||
style: TextStyle(
|
||||
color: context.tokens.danger, fontSize: 12)),
|
||||
data: (warehouses) => DsSelect<Warehouse>(
|
||||
value: _selectedWarehouse,
|
||||
hint: '请选择仓库',
|
||||
options: [
|
||||
for (final w in warehouses) (w, w.name),
|
||||
],
|
||||
onChanged: (w) {
|
||||
setState(() => _selectedWarehouse = w);
|
||||
_loadInventory(w);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
_InfoField(
|
||||
label: '盘点类型',
|
||||
child: DsSelect<String>(
|
||||
value: _checkType,
|
||||
options: const [
|
||||
('全盘', '全盘'),
|
||||
('抽盘', '抽盘'),
|
||||
('循环盘点', '循环盘点'),
|
||||
],
|
||||
onChanged: (v) => setState(() => _checkType = v),
|
||||
),
|
||||
),
|
||||
_InfoField(
|
||||
label: '盘点日期',
|
||||
child: InputDecorator(
|
||||
decoration: const InputDecoration(),
|
||||
child: Text(_dateStr, style: const TextStyle(fontSize: 13)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ── 窄屏(原型 m-inventory-check 粒度)──────────────────────────────
|
||||
|
||||
int get _totalDiff =>
|
||||
_checkItems.fold(0, (sum, item) => sum + _getDiff(item));
|
||||
|
||||
String get _scopeLabel =>
|
||||
'${_selectedWarehouse?.name ?? '未选仓库'} · $_checkType';
|
||||
|
||||
String _fmtDiff(int diff) => diff > 0 ? '+$diff' : '$diff';
|
||||
|
||||
Widget _buildMobile(AsyncValue<List<Warehouse>> asyncWarehouses) {
|
||||
final t = context.tokens;
|
||||
return Scaffold(
|
||||
backgroundColor: t.bg,
|
||||
body: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(AppDims.sp3),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
_basicInfoCard(asyncWarehouses),
|
||||
// .m-section
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(2, 16, 2, 8),
|
||||
child: Text('盘点单',
|
||||
style: TextStyle(
|
||||
fontSize: AppDims.fsSm,
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: .4,
|
||||
color: t.muted)),
|
||||
),
|
||||
if (_loadingItems)
|
||||
const Padding(
|
||||
padding: EdgeInsets.all(24),
|
||||
child: Center(child: CircularProgressIndicator()),
|
||||
)
|
||||
else
|
||||
_draftCard(t),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
// 底部操作条(原型 .m-actionbar 形态:横排等分按钮)
|
||||
Container(
|
||||
padding: const EdgeInsets.fromLTRB(16, 12, 16, 12),
|
||||
decoration: BoxDecoration(
|
||||
color: t.surface,
|
||||
border: Border(top: BorderSide(color: t.borderSubtle)),
|
||||
),
|
||||
child: Row(children: [
|
||||
Expanded(
|
||||
child: DsButton('取消',
|
||||
onPressed: () => context.go('/inventory')),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: DsButton('提交盘点',
|
||||
variant: DsBtnVariant.primary,
|
||||
onPressed: (_submitting || _checkItems.isEmpty)
|
||||
? null
|
||||
: _submit),
|
||||
),
|
||||
]),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 盘点单卡(原型 .m-card:单号 / 范围·项数 / 状态徽章 + 差异 / 日期脚注)。
|
||||
Widget _draftCard(dynamic t) {
|
||||
final diff = _totalDiff;
|
||||
return MobileListCard(
|
||||
onTap: _openDraftSheet,
|
||||
title: Text(_checkNo,
|
||||
style: const TextStyle(
|
||||
fontFamily: AppFonts.mono,
|
||||
fontFamilyFallback: AppFonts.monoFallback)),
|
||||
subtitle: Text('$_scopeLabel · ${_checkItems.length} 项'),
|
||||
trailing: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
DsBadge('进行中',
|
||||
tone: DsBadgeTone.warn, icon: statusIcon('进行中')),
|
||||
const SizedBox(height: 6),
|
||||
// 「差异」为 CJK 走默认字体,数值走 mono(JetBrains Mono 无 CJK 字形)
|
||||
Text.rich(
|
||||
TextSpan(children: [
|
||||
TextSpan(
|
||||
text: '差异 ',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w700,
|
||||
fontSize: 13,
|
||||
color: diff == 0 ? t.success : t.danger)),
|
||||
TextSpan(
|
||||
text: _fmtDiff(diff),
|
||||
style: TextStyle(
|
||||
fontFamily: AppFonts.mono,
|
||||
fontFamilyFallback: AppFonts.monoFallback,
|
||||
fontWeight: FontWeight.w700,
|
||||
fontSize: 13,
|
||||
color: diff == 0 ? t.success : t.danger)),
|
||||
]),
|
||||
),
|
||||
],
|
||||
),
|
||||
fields: [MobileCardField('日期', _dateStr)],
|
||||
);
|
||||
}
|
||||
|
||||
/// 盘点单详情 sheet(原型 openSheet:.drow 键值行 + 状态徽章 + 继续录入)。
|
||||
void _openDraftSheet() {
|
||||
final t = context.tokens;
|
||||
final diff = _totalDiff;
|
||||
showMSheet<void>(
|
||||
context,
|
||||
title: '盘点单详情',
|
||||
builder: (ctx) => Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_drow(ctx, '单号', value: _checkNo, mono: true),
|
||||
_drow(ctx, '范围', value: _scopeLabel),
|
||||
_drow(ctx, '商品项数', value: '${_checkItems.length} 项'),
|
||||
_drow(ctx, '盈亏差异',
|
||||
value: _fmtDiff(diff),
|
||||
mono: true,
|
||||
color: diff == 0 ? t.success : t.danger),
|
||||
_drow(ctx, '状态',
|
||||
child: DsBadge('进行中',
|
||||
tone: DsBadgeTone.warn, icon: statusIcon('进行中')),
|
||||
last: true),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
DsButton('继续录入',
|
||||
variant: DsBtnVariant.primary,
|
||||
onPressed: () => Navigator.of(context).pop()),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// 原型 .drow:label(muted) + b(text 600),pad 11 0,下边 border-subtle。
|
||||
Widget _drow(BuildContext ctx, String label,
|
||||
{String? value,
|
||||
Widget? child,
|
||||
bool mono = false,
|
||||
Color? color,
|
||||
bool last = false}) {
|
||||
final t = ctx.tokens;
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 11),
|
||||
decoration: BoxDecoration(
|
||||
border:
|
||||
last ? null : Border(bottom: BorderSide(color: t.borderSubtle))),
|
||||
child: Row(children: [
|
||||
Text(label,
|
||||
style: TextStyle(fontSize: AppDims.fsBody, color: t.muted)),
|
||||
const Spacer(),
|
||||
child ??
|
||||
Text(value ?? '',
|
||||
style: TextStyle(
|
||||
fontSize: AppDims.fsBody,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontFamily: mono ? AppFonts.mono : null,
|
||||
fontFamilyFallback: mono ? AppFonts.monoFallback : null,
|
||||
color: color ?? t.text)),
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
TableRow _buildCheckRow(int index) {
|
||||
final item = _checkItems[index];
|
||||
final diff = _getDiff(item);
|
||||
|
||||
Reference in New Issue
Block a user