dde87a32b6
- 库存列表:窄屏隐藏页内大标题头;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>
734 lines
27 KiB
Dart
734 lines
27 KiB
Dart
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
||
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';
|
||
import '../../providers/inventory_provider.dart';
|
||
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});
|
||
|
||
@override
|
||
ConsumerState<InventoryCheckScreen> createState() =>
|
||
_InventoryCheckScreenState();
|
||
}
|
||
|
||
class _InventoryCheckScreenState extends ConsumerState<InventoryCheckScreen> {
|
||
Warehouse? _selectedWarehouse;
|
||
String _checkType = '全盘';
|
||
bool _submitting = false;
|
||
bool _loadingItems = false;
|
||
|
||
// Editable check items built from real inventory
|
||
final List<_CheckItem> _checkItems = [];
|
||
|
||
String get _checkNo {
|
||
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) {
|
||
item.dispose();
|
||
}
|
||
super.dispose();
|
||
}
|
||
|
||
Future<void> _loadInventory(Warehouse wh) async {
|
||
setState(() {
|
||
_loadingItems = true;
|
||
for (final item in _checkItems) {
|
||
item.dispose();
|
||
}
|
||
_checkItems.clear();
|
||
});
|
||
try {
|
||
final result = await ref.read(inventoryRepositoryProvider).listInventory(
|
||
warehouseId: wh.id, pageSize: AppConstants.inventoryCheckPageSize);
|
||
final items = result.data
|
||
.where((inv) => inv.productId != null)
|
||
.map((inv) => _CheckItem(inventory: inv))
|
||
.toList();
|
||
if (mounted) {
|
||
setState(() {
|
||
_checkItems.addAll(items);
|
||
_loadingItems = false;
|
||
});
|
||
}
|
||
} catch (e) {
|
||
if (mounted) {
|
||
setState(() => _loadingItems = false);
|
||
showDsToast(context, '加载库存失败:$e', bg: context.tokens.danger);
|
||
}
|
||
}
|
||
}
|
||
|
||
Future<void> _submit() async {
|
||
if (_selectedWarehouse == null) {
|
||
showDsToast(context, '请先选择仓库');
|
||
return;
|
||
}
|
||
setState(() => _submitting = true);
|
||
try {
|
||
final dateStr = _dateStr;
|
||
|
||
final items = _checkItems.map((item) {
|
||
final actual =
|
||
double.tryParse(item.actualQtyCtrl.text) ?? item.inventory.quantity;
|
||
return {
|
||
'product_id': item.inventory.productId!,
|
||
'actual_qty': actual,
|
||
'remark': item.remarkCtrl.text.trim(),
|
||
};
|
||
}).toList();
|
||
|
||
await ref.read(inventoryRepositoryProvider).createCheck({
|
||
'check_no': _checkNo,
|
||
'warehouse_id': _selectedWarehouse!.id,
|
||
'check_date': dateStr,
|
||
'items': items,
|
||
});
|
||
|
||
if (mounted) {
|
||
showDsToast(context, '盘点单已提交', bg: context.tokens.success);
|
||
context.go('/inventory');
|
||
}
|
||
} catch (e) {
|
||
if (mounted) {
|
||
showDsToast(context, '提交失败:$e', bg: context.tokens.danger);
|
||
}
|
||
} finally {
|
||
if (mounted) setState(() => _submitting = false);
|
||
}
|
||
}
|
||
|
||
int _getDiff(_CheckItem item) {
|
||
final actual =
|
||
double.tryParse(item.actualQtyCtrl.text) ?? item.inventory.quantity;
|
||
return (actual - item.inventory.quantity).round();
|
||
}
|
||
|
||
@override
|
||
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(
|
||
children: [
|
||
// Header
|
||
Container(
|
||
height: 52,
|
||
color: context.tokens.surface,
|
||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||
child: Row(
|
||
children: [
|
||
IconButton(
|
||
icon: const Icon(LucideIcons.arrowLeft, size: 20),
|
||
onPressed: () => context.go('/inventory'),
|
||
),
|
||
const SizedBox(width: 8),
|
||
const Text('库存盘点',
|
||
style:
|
||
TextStyle(fontSize: 16, fontWeight: FontWeight.w600)),
|
||
const Spacer(),
|
||
OutlinedButton(
|
||
onPressed: () => context.go('/inventory'),
|
||
child: const Text('取消'),
|
||
),
|
||
const SizedBox(width: 8),
|
||
ElevatedButton.icon(
|
||
onPressed:
|
||
(_submitting || _checkItems.isEmpty) ? null : _submit,
|
||
icon: _submitting
|
||
? const SizedBox(
|
||
width: 14,
|
||
height: 14,
|
||
child: CircularProgressIndicator(
|
||
strokeWidth: 2,
|
||
color: Colors.white)) // ds-ignore: 盘点屏待重建批次统一
|
||
: const Icon(LucideIcons.circleCheck, size: 16),
|
||
label: const Text('提交盘点'),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
const Divider(height: 1),
|
||
Expanded(
|
||
child: SingleChildScrollView(
|
||
padding: const EdgeInsets.all(16),
|
||
child: Column(
|
||
children: [
|
||
// Basic info
|
||
_basicInfoCard(asyncWarehouses),
|
||
const SizedBox(height: 12),
|
||
// Check items table
|
||
Card(
|
||
child: Padding(
|
||
padding: const EdgeInsets.all(16),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Row(
|
||
children: [
|
||
Text('盘点明细',
|
||
style: TextStyle(
|
||
fontSize: 14,
|
||
fontWeight: FontWeight.w600,
|
||
color: context.tokens.primaryDark)),
|
||
const SizedBox(width: 12),
|
||
if (_checkItems.isNotEmpty)
|
||
Container(
|
||
padding: const EdgeInsets.symmetric(
|
||
horizontal: 8, vertical: 2),
|
||
decoration: BoxDecoration(
|
||
color:
|
||
context.tokens.accent.withOpacity(0.1),
|
||
borderRadius: BorderRadius.circular(3),
|
||
),
|
||
child: Text(
|
||
'差异 ${_checkItems.where((i) => _getDiff(i) != 0).length} 项',
|
||
style: TextStyle(
|
||
fontSize: 12,
|
||
color: context.tokens.accent),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
const SizedBox(height: 12),
|
||
if (_selectedWarehouse == null)
|
||
Center(
|
||
child: Padding(
|
||
padding: const EdgeInsets.all(32),
|
||
child: Text('请先选择盘点仓库',
|
||
style:
|
||
TextStyle(color: context.tokens.muted)),
|
||
),
|
||
)
|
||
else if (_loadingItems)
|
||
const Center(
|
||
child: Padding(
|
||
padding: EdgeInsets.all(32),
|
||
child: CircularProgressIndicator(),
|
||
),
|
||
)
|
||
else if (_checkItems.isEmpty)
|
||
Center(
|
||
child: Padding(
|
||
padding: const EdgeInsets.all(32),
|
||
child: Text('该仓库暂无库存记录',
|
||
style:
|
||
TextStyle(color: context.tokens.muted)),
|
||
),
|
||
)
|
||
else
|
||
Table(
|
||
columnWidths: const {
|
||
0: FixedColumnWidth(36),
|
||
1: FixedColumnWidth(80),
|
||
2: FlexColumnWidth(3),
|
||
3: FixedColumnWidth(50),
|
||
4: FixedColumnWidth(80),
|
||
5: FixedColumnWidth(120),
|
||
6: FixedColumnWidth(80),
|
||
7: FlexColumnWidth(2),
|
||
},
|
||
children: [
|
||
TableRow(
|
||
decoration:
|
||
BoxDecoration(color: context.tokens.thBg),
|
||
children: [
|
||
'序号',
|
||
'商品编码',
|
||
'商品名称',
|
||
'单位',
|
||
'账面数量',
|
||
'实际数量',
|
||
'差异',
|
||
'备注',
|
||
]
|
||
.map((h) => Padding(
|
||
padding: const EdgeInsets.symmetric(
|
||
horizontal: 8, vertical: 10),
|
||
child: Text(h,
|
||
style: TextStyle(
|
||
fontSize: 13,
|
||
fontWeight: FontWeight.w600,
|
||
color: context
|
||
.tokens.primaryDark)),
|
||
))
|
||
.toList(),
|
||
),
|
||
...List.generate(_checkItems.length,
|
||
(i) => _buildCheckRow(i)),
|
||
],
|
||
),
|
||
if (_checkItems.isNotEmpty) ...[
|
||
const Divider(height: 1),
|
||
Padding(
|
||
padding: const EdgeInsets.only(top: 12),
|
||
child: Row(
|
||
children: [
|
||
_SummaryItem(
|
||
label: '盘点商品',
|
||
value: '${_checkItems.length}种',
|
||
color: context.tokens.primary,
|
||
),
|
||
const SizedBox(width: 24),
|
||
_SummaryItem(
|
||
label: '盘盈',
|
||
value:
|
||
'${_checkItems.where((i) => _getDiff(i) > 0).length}种',
|
||
color: context.tokens.success,
|
||
),
|
||
const SizedBox(width: 24),
|
||
_SummaryItem(
|
||
label: '盘亏',
|
||
value:
|
||
'${_checkItems.where((i) => _getDiff(i) < 0).length}种',
|
||
color: context.tokens.danger,
|
||
),
|
||
const SizedBox(width: 24),
|
||
_SummaryItem(
|
||
label: '相符',
|
||
value:
|
||
'${_checkItems.where((i) => _getDiff(i) == 0).length}种',
|
||
color: context.tokens.muted,
|
||
),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
],
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
/// 盘点基本信息卡(桌面/移动共用):单号 / 仓库 / 类型 / 日期。
|
||
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);
|
||
final inv = item.inventory;
|
||
|
||
return TableRow(
|
||
decoration: BoxDecoration(
|
||
color: diff != 0
|
||
? (diff > 0
|
||
? context.tokens.success.withOpacity(0.04)
|
||
: context.tokens.danger.withOpacity(0.04))
|
||
: (index.isEven ? context.tokens.surface : context.tokens.bg),
|
||
),
|
||
children: [
|
||
Padding(
|
||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 10),
|
||
child: Text('${index + 1}',
|
||
style: TextStyle(fontSize: 13, color: context.tokens.muted)),
|
||
),
|
||
Padding(
|
||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 10),
|
||
child: Text(inv.productCode.isEmpty ? '-' : inv.productCode,
|
||
style: TextStyle(
|
||
fontFamily: AppFonts.mono,
|
||
fontFamilyFallback: AppFonts.monoFallback,
|
||
fontSize: 12,
|
||
color: context.tokens.muted)),
|
||
),
|
||
Padding(
|
||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 10),
|
||
child: Text(inv.productName.isEmpty ? '-' : inv.productName,
|
||
style: const TextStyle(fontSize: 13),
|
||
overflow: TextOverflow.ellipsis),
|
||
),
|
||
Padding(
|
||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 10),
|
||
child: Text(inv.unit.isEmpty ? '-' : inv.unit,
|
||
style: const TextStyle(fontSize: 13)),
|
||
),
|
||
Padding(
|
||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 10),
|
||
child: Text(inv.quantity.toStringAsFixed(0),
|
||
style: const TextStyle(fontSize: 13)),
|
||
),
|
||
Padding(
|
||
padding: const EdgeInsets.all(4),
|
||
child: TextFormField(
|
||
controller: item.actualQtyCtrl,
|
||
decoration: const InputDecoration(),
|
||
style: const TextStyle(fontSize: 13),
|
||
keyboardType: TextInputType.number,
|
||
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
||
onChanged: (_) => setState(() {}),
|
||
),
|
||
),
|
||
Padding(
|
||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 10),
|
||
child: Text(
|
||
diff == 0 ? '0' : (diff > 0 ? '+$diff' : '$diff'),
|
||
style: TextStyle(
|
||
fontSize: 13,
|
||
fontWeight: FontWeight.w600,
|
||
color: diff == 0
|
||
? context.tokens.muted
|
||
: (diff > 0 ? context.tokens.success : context.tokens.danger),
|
||
),
|
||
),
|
||
),
|
||
Padding(
|
||
padding: const EdgeInsets.all(4),
|
||
child: TextFormField(
|
||
controller: item.remarkCtrl,
|
||
decoration: const InputDecoration(hintText: '备注'),
|
||
style: const TextStyle(fontSize: 13),
|
||
),
|
||
),
|
||
],
|
||
);
|
||
}
|
||
}
|
||
|
||
class _CheckItem {
|
||
final Inventory inventory;
|
||
final TextEditingController actualQtyCtrl;
|
||
final TextEditingController remarkCtrl;
|
||
|
||
_CheckItem({required this.inventory})
|
||
: actualQtyCtrl =
|
||
TextEditingController(text: inventory.quantity.toStringAsFixed(0)),
|
||
remarkCtrl = TextEditingController();
|
||
|
||
void dispose() {
|
||
actualQtyCtrl.dispose();
|
||
remarkCtrl.dispose();
|
||
}
|
||
}
|
||
|
||
class _InfoField extends StatelessWidget {
|
||
final String label;
|
||
final Widget child;
|
||
|
||
const _InfoField({required this.label, required this.child});
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return SizedBox(
|
||
width: 220,
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(label,
|
||
style: TextStyle(fontSize: 13, color: context.tokens.muted)),
|
||
const SizedBox(height: 6),
|
||
child,
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
class _SummaryItem extends StatelessWidget {
|
||
final String label;
|
||
final String value;
|
||
final Color color;
|
||
|
||
const _SummaryItem(
|
||
{required this.label, required this.value, required this.color});
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Row(
|
||
children: [
|
||
Text(label,
|
||
style: TextStyle(fontSize: 13, color: context.tokens.muted)),
|
||
const SizedBox(width: 4),
|
||
Text(value,
|
||
style: TextStyle(
|
||
fontSize: 14, fontWeight: FontWeight.w600, color: color)),
|
||
],
|
||
);
|
||
}
|
||
}
|