feat(client): 出入库列表窄屏对齐移动原型(KPI 筛选/搜索行/状态与详搜 sheet/图标徽章)
- 窄屏隐藏页内大标题头与新增/导出/打印入口(移动端无建单与打印——拍板) - KPI 改 MKpiGrid 2×2:待审核/草稿卡可点切换状态筛选(再点取消,selected 联动) - 工具栏改 MSearchRow:搜索(provider 内置防抖)+ 纯文字状态钮 + 图标详搜钮 - 状态筛选/详细搜索改 showMSheet 底部 sheet(详搜字段与桌面完全同集,重置/应用在 actions) - 卡片流徽章换图标变体(含派生态待定价/部分退单/已退单)+ › 箭头,行内操作收进详情 sheet - 确认进价/确认售价窄屏走 showMSheet;窄屏详情操作组不出打印 - 新增 390×844 三主题 golden(m_stock_in_list/m_stock_out_list);重生成既有 390×1300 移动 golden 基准(桌面 golden 零漂移) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@@ -17,6 +17,10 @@ import '../../providers/stock_in_provider.dart';
|
|||||||
import '../../widgets/ds/ds_atoms.dart';
|
import '../../widgets/ds/ds_atoms.dart';
|
||||||
import '../../widgets/ds/ds_kpi.dart';
|
import '../../widgets/ds/ds_kpi.dart';
|
||||||
import '../../widgets/ds/ds_table.dart';
|
import '../../widgets/ds/ds_table.dart';
|
||||||
|
import '../../widgets/ds/m_kpi_grid.dart';
|
||||||
|
import '../../widgets/ds/m_search_row.dart';
|
||||||
|
import '../../widgets/ds/m_sheet.dart';
|
||||||
|
import '../../widgets/ds/status_icon_map.dart';
|
||||||
import '../../widgets/mobile_list_card.dart';
|
import '../../widgets/mobile_list_card.dart';
|
||||||
import '../../widgets/searchable_option_field.dart';
|
import '../../widgets/searchable_option_field.dart';
|
||||||
import '../../widgets/combo_search_field.dart';
|
import '../../widgets/combo_search_field.dart';
|
||||||
@@ -32,7 +36,6 @@ import '../../providers/finance_provider.dart'
|
|||||||
show financeRepositoryProvider, financePartnerRowsProvider;
|
show financeRepositoryProvider, financePartnerRowsProvider;
|
||||||
import '../../providers/shop_provider.dart' show shopInfoProvider;
|
import '../../providers/shop_provider.dart' show shopInfoProvider;
|
||||||
import '../../widgets/write_guard.dart';
|
import '../../widgets/write_guard.dart';
|
||||||
import '../../widgets/order_row_actions.dart';
|
|
||||||
import '../../widgets/order_return_dialog.dart';
|
import '../../widgets/order_return_dialog.dart';
|
||||||
import '../../widgets/order_detail_drawer.dart';
|
import '../../widgets/order_detail_drawer.dart';
|
||||||
import '../../widgets/wheel_date_picker.dart';
|
import '../../widgets/wheel_date_picker.dart';
|
||||||
@@ -265,24 +268,47 @@ class _StockInListScreenState extends ConsumerState<StockInListScreen> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
Future<void> _openAdvSearch() async {
|
Future<void> _openAdvSearch() async {
|
||||||
final res = await showAppDialog<_AdvResult>(
|
final _AdvResult? res;
|
||||||
context: context,
|
if (context.isMobile) {
|
||||||
builder: (_) => _AdvSearchDialog(
|
// 窄屏:底部 sheet 竖排单列(字段与桌面同集,原型 drawAdv)。
|
||||||
initialDetail: _detail,
|
final formKey = GlobalKey<_AdvSearchSheetState>();
|
||||||
initialStatus: _statusFilter,
|
res = await showMSheet<_AdvResult>(
|
||||||
initialRange: _dateRange,
|
context,
|
||||||
),
|
title: '详细搜索',
|
||||||
);
|
builder: (_) => _AdvSearchSheet(
|
||||||
if (res == null || !mounted) return;
|
key: formKey,
|
||||||
|
initialDetail: _detail,
|
||||||
|
initialStatus: _statusFilter,
|
||||||
|
initialRange: _dateRange,
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
DsButton('重置', onPressed: () => formKey.currentState?.reset()),
|
||||||
|
DsButton('应用',
|
||||||
|
variant: DsBtnVariant.primary,
|
||||||
|
onPressed: () => formKey.currentState?.apply()),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
res = await showAppDialog<_AdvResult>(
|
||||||
|
context: context,
|
||||||
|
builder: (_) => _AdvSearchDialog(
|
||||||
|
initialDetail: _detail,
|
||||||
|
initialStatus: _statusFilter,
|
||||||
|
initialRange: _dateRange,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
final r = res;
|
||||||
|
if (r == null || !mounted) return;
|
||||||
setState(() {
|
setState(() {
|
||||||
_detail = res.detail;
|
_detail = r.detail;
|
||||||
_statusFilter = res.status;
|
_statusFilter = r.status;
|
||||||
_dateRange = res.dateRange;
|
_dateRange = r.dateRange;
|
||||||
});
|
});
|
||||||
final n = ref.read(stockInListProvider.notifier);
|
final n = ref.read(stockInListProvider.notifier);
|
||||||
n.setStatus(res.status);
|
n.setStatus(r.status);
|
||||||
n.setDateRange(_startDate, _endDate);
|
n.setDateRange(_startDate, _endDate);
|
||||||
n.setDetail(res.detail);
|
n.setDetail(r.detail);
|
||||||
}
|
}
|
||||||
|
|
||||||
int get _advCount {
|
int get _advCount {
|
||||||
@@ -346,16 +372,25 @@ class _StockInListScreenState extends ConsumerState<StockInListScreen> {
|
|||||||
|
|
||||||
final summary = ref.watch(stockInSummary30Provider).valueOrNull;
|
final summary = ref.watch(stockInSummary30Provider).valueOrNull;
|
||||||
|
|
||||||
|
final mobile = context.isMobile;
|
||||||
final content = Container(
|
final content = Container(
|
||||||
color: context.tokens.bg,
|
color: context.tokens.bg,
|
||||||
padding: context.isMobile
|
padding: mobile
|
||||||
? EdgeInsets.zero
|
? EdgeInsets.zero
|
||||||
// 原型 .main{padding:22px 26px}
|
// 原型 .main{padding:22px 26px}
|
||||||
: const EdgeInsets.fromLTRB(26, 22, 26, 22),
|
: const EdgeInsets.fromLTRB(26, 22, 26, 22),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
_buildHeader(result.total, summary),
|
// 窄屏对齐移动原型 m-stock-in-list:标题在顶栏、正文从 KPI 起;
|
||||||
_buildKpis(summary, result.total),
|
// 无页头按钮(移动端无建单/导出/打印入口——用户拍板)。
|
||||||
|
if (!mobile) ...[
|
||||||
|
_buildHeader(result.total, summary),
|
||||||
|
_buildKpis(summary, result.total),
|
||||||
|
] else ...[
|
||||||
|
_buildMobileKpis(summary, result.total),
|
||||||
|
_buildMobileSearchRow(),
|
||||||
|
_buildMobileSection(result.total),
|
||||||
|
],
|
||||||
Expanded(
|
Expanded(
|
||||||
child: DsTable(
|
child: DsTable(
|
||||||
total: result.total,
|
total: result.total,
|
||||||
@@ -366,12 +401,14 @@ class _StockInListScreenState extends ConsumerState<StockInListScreen> {
|
|||||||
onPageSizeChanged: (s) =>
|
onPageSizeChanged: (s) =>
|
||||||
ref.read(stockInListProvider.notifier).setPageSize(s),
|
ref.read(stockInListProvider.notifier).setPageSize(s),
|
||||||
emptyText: '没有匹配的入库单 · 试试调整筛选或搜索',
|
emptyText: '没有匹配的入库单 · 试试调整筛选或搜索',
|
||||||
toolbar: _buildToolbar(
|
toolbar: mobile
|
||||||
orders: orders,
|
? null
|
||||||
warehouseOptions: warehouseOptions,
|
: _buildToolbar(
|
||||||
supplierOptions: supplierOptions,
|
orders: orders,
|
||||||
selectedSupplierId: selectedSupplierId,
|
warehouseOptions: warehouseOptions,
|
||||||
),
|
supplierOptions: supplierOptions,
|
||||||
|
selectedSupplierId: selectedSupplierId,
|
||||||
|
),
|
||||||
mobileCards:
|
mobileCards:
|
||||||
orders.map((o) => _orderCard(context, o)).toList(),
|
orders.map((o) => _orderCard(context, o)).toList(),
|
||||||
columns: _colDefs
|
columns: _colDefs
|
||||||
@@ -534,6 +571,178 @@ class _StockInListScreenState extends ConsumerState<StockInListScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── 窄屏形态(对齐移动原型 m-stock-in-list.html)──────────────────
|
||||||
|
|
||||||
|
/// 状态词 → (前景, 软底):镜像原型 .badge.b-*,全走 token。
|
||||||
|
(Color, Color) _statusColors(String label) {
|
||||||
|
final t = context.tokens;
|
||||||
|
return switch (label) {
|
||||||
|
'待审核' => (t.warn, t.warnBg),
|
||||||
|
'已审核' => (t.success, t.successBg),
|
||||||
|
'已拒绝' => (t.danger, t.dangerBg),
|
||||||
|
'部分退单' => (t.warn, t.warnBg),
|
||||||
|
'已退单' => (t.danger, t.dangerBg),
|
||||||
|
'待定价' => (t.warn, t.warnBg),
|
||||||
|
_ => (t.muted, t.infoSoft), // 草稿
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 原型 .badge.ico:纯图标小徽章(卡片右上态标 / 状态 sheet 选项行)。
|
||||||
|
Widget _icoBadge(String label) {
|
||||||
|
final (fg, bg) = _statusColors(label);
|
||||||
|
return Container(
|
||||||
|
height: 22,
|
||||||
|
width: 26,
|
||||||
|
alignment: Alignment.center,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: bg,
|
||||||
|
borderRadius: BorderRadius.circular(AppDims.rPill),
|
||||||
|
),
|
||||||
|
child: Icon(statusIcon(label), size: 12, color: fg),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 原型 .m-kpi 2×2:近30天笔数/金额 + 可点筛选的待审核/草稿(再点取消)。
|
||||||
|
Widget _buildMobileKpis(StockSummary? summary, int total) {
|
||||||
|
String yuanWan(double v) => v >= 10000
|
||||||
|
? '¥${(v / 10000).toStringAsFixed(v >= 1000000 ? 0 : 1)}万'
|
||||||
|
: '¥${v.toStringAsFixed(0)}';
|
||||||
|
String pct(double? p) => p == null
|
||||||
|
? '较上期 —'
|
||||||
|
: '${p >= 0 ? '▲' : '▼'} ${p.abs().toStringAsFixed(1)}% 较上期';
|
||||||
|
MKpiDeltaTone tone(double? p) => p == null
|
||||||
|
? MKpiDeltaTone.normal
|
||||||
|
: (p >= 0 ? MKpiDeltaTone.up : MKpiDeltaTone.down);
|
||||||
|
final pendingCount = summary?.pendingCount ?? 0;
|
||||||
|
final draftCount = summary?.draftCount ?? 0;
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(12, 12, 12, 0),
|
||||||
|
child: MKpiGrid(items: [
|
||||||
|
MKpiItem(
|
||||||
|
label: '近30天入库笔数',
|
||||||
|
value: NumberFormat.decimalPattern()
|
||||||
|
.format(summary?.monthCount ?? total),
|
||||||
|
icon: LucideIcons.download,
|
||||||
|
delta: pct(summary?.countDeltaPct),
|
||||||
|
deltaTone: tone(summary?.countDeltaPct)),
|
||||||
|
MKpiItem(
|
||||||
|
label: '近30天入库金额',
|
||||||
|
value: summary != null ? yuanWan(summary.monthAmount) : '—',
|
||||||
|
icon: LucideIcons.wallet,
|
||||||
|
delta: pct(summary?.amountDeltaPct),
|
||||||
|
deltaTone: tone(summary?.amountDeltaPct)),
|
||||||
|
MKpiItem(
|
||||||
|
label: '待审核 · 点击筛选',
|
||||||
|
value: '$pendingCount',
|
||||||
|
icon: LucideIcons.clock,
|
||||||
|
delta: pendingCount > 0 ? '需尽快处理' : '点击筛选',
|
||||||
|
deltaTone:
|
||||||
|
pendingCount > 0 ? MKpiDeltaTone.warn : MKpiDeltaTone.normal,
|
||||||
|
selected: _statusFilter == 'pending',
|
||||||
|
onTap: () =>
|
||||||
|
_setStatus(_statusFilter == 'pending' ? '' : 'pending')),
|
||||||
|
MKpiItem(
|
||||||
|
label: '草稿 · 点击筛选',
|
||||||
|
value: '$draftCount',
|
||||||
|
icon: LucideIcons.fileText,
|
||||||
|
delta: draftCount > 0 ? '待提交审核' : '点击筛选',
|
||||||
|
selected: _statusFilter == 'draft',
|
||||||
|
onTap: () => _setStatus(_statusFilter == 'draft' ? '' : 'draft')),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 详细搜索是否有生效条件(不含状态钮自己的 return_state)→ 详搜钮高亮。
|
||||||
|
bool get _mobileAdvActive =>
|
||||||
|
_detail.entries
|
||||||
|
.any((e) => e.key != 'return_state' && e.value.trim().isNotEmpty) ||
|
||||||
|
_dateRange != null;
|
||||||
|
|
||||||
|
/// 原型搜索区:搜索框 + 纯文字状态钮 + 图标详搜钮。
|
||||||
|
Widget _buildMobileSearchRow() {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(12, 10, 12, 0),
|
||||||
|
child: MSearchRow(
|
||||||
|
controller: _searchCtrl,
|
||||||
|
hint: '搜索单号 / 供应商 / 酒名',
|
||||||
|
// setKeyword 内置 350ms 防抖 + 同词去重,oninput 直连即可(对齐原型)。
|
||||||
|
onChanged: (v) => ref.read(stockInListProvider.notifier).setKeyword(v),
|
||||||
|
onSubmitted: (v) =>
|
||||||
|
ref.read(stockInListProvider.notifier).setKeyword(v),
|
||||||
|
statusLabel: _statusLabelOf,
|
||||||
|
statusActive: _statusFilter.isNotEmpty,
|
||||||
|
onStatusTap: _openStatusSheet,
|
||||||
|
filterActive: _mobileAdvActive,
|
||||||
|
onFilterTap: _openAdvSearch,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 原型 .m-section:入库单 · 共 N。
|
||||||
|
Widget _buildMobileSection(int total) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(14, 14, 14, 8),
|
||||||
|
child: Align(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
child: Text('入库单 · 共 $total',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: AppDims.fsSm,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
letterSpacing: .4,
|
||||||
|
color: context.tokens.muted)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 状态筛选底部 sheet(原型 openStatusSheet 的 m-opt 列表:图标 + 状态词 + 勾)。
|
||||||
|
Future<void> _openStatusSheet() async {
|
||||||
|
final sel = await showMSheet<String>(
|
||||||
|
context,
|
||||||
|
title: '单据状态',
|
||||||
|
builder: (ctx) {
|
||||||
|
final t = ctx.tokens;
|
||||||
|
return Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
for (var i = 0; i < _statusOptions.length; i++)
|
||||||
|
InkWell(
|
||||||
|
onTap: () => Navigator.of(ctx).pop(_statusOptions[i].$1),
|
||||||
|
child: Container(
|
||||||
|
padding:
|
||||||
|
const EdgeInsets.symmetric(vertical: 13, horizontal: 4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: i < _statusOptions.length - 1
|
||||||
|
? Border(bottom: BorderSide(color: t.borderSubtle))
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
child: Row(children: [
|
||||||
|
if (_statusOptions[i].$2 != '全部') ...[
|
||||||
|
_icoBadge(_statusOptions[i].$2),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
],
|
||||||
|
Text(_statusOptions[i].$2,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: AppDims.fsBody,
|
||||||
|
fontWeight: _statusOptions[i].$1 == _statusFilter
|
||||||
|
? FontWeight.w600
|
||||||
|
: FontWeight.w400,
|
||||||
|
color: _statusOptions[i].$1 == _statusFilter
|
||||||
|
? t.primary
|
||||||
|
: t.text)),
|
||||||
|
const Spacer(),
|
||||||
|
if (_statusOptions[i].$1 == _statusFilter)
|
||||||
|
Icon(LucideIcons.check, size: 18, color: t.primary),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (sel == null || !mounted) return;
|
||||||
|
_setStatus(sel);
|
||||||
|
}
|
||||||
|
|
||||||
// ── 工具栏(原型 .toolbar)─────────────────────────────────────────
|
// ── 工具栏(原型 .toolbar)─────────────────────────────────────────
|
||||||
Widget _buildToolbar({
|
Widget _buildToolbar({
|
||||||
required List<StockInOrder> orders,
|
required List<StockInOrder> orders,
|
||||||
@@ -542,8 +751,6 @@ class _StockInListScreenState extends ConsumerState<StockInListScreen> {
|
|||||||
required int? selectedSupplierId,
|
required int? selectedSupplierId,
|
||||||
}) {
|
}) {
|
||||||
return Builder(builder: (ctx) {
|
return Builder(builder: (ctx) {
|
||||||
final isMobile = ctx.isMobile;
|
|
||||||
|
|
||||||
final searchField = DsSearchBox(
|
final searchField = DsSearchBox(
|
||||||
controller: _searchCtrl,
|
controller: _searchCtrl,
|
||||||
hint: '单号 / 酒名 / 商品编码',
|
hint: '单号 / 酒名 / 商品编码',
|
||||||
@@ -613,49 +820,8 @@ class _StockInListScreenState extends ConsumerState<StockInListScreen> {
|
|||||||
onPressed: _resetFilters,
|
onPressed: _resetFilters,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isMobile) {
|
|
||||||
return Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
searchField,
|
|
||||||
const SizedBox(height: AppDims.sp2),
|
|
||||||
Wrap(
|
|
||||||
spacing: AppDims.sp2,
|
|
||||||
runSpacing: AppDims.sp2,
|
|
||||||
crossAxisAlignment: WrapCrossAlignment.center,
|
|
||||||
children: [
|
|
||||||
if (!WriteGuard.isReadonly(ref))
|
|
||||||
WriteGuard(
|
|
||||||
child: DsButton(
|
|
||||||
'新增',
|
|
||||||
icon: LucideIcons.plus,
|
|
||||||
small: true,
|
|
||||||
variant: DsBtnVariant.primary,
|
|
||||||
onPressed: () => context.go('/stock-in/new'),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
supplierField,
|
|
||||||
warehouseChip,
|
|
||||||
statusChip,
|
|
||||||
dateChip,
|
|
||||||
advBtn,
|
|
||||||
resetBtn,
|
|
||||||
DsButton('导出',
|
|
||||||
icon: LucideIcons.download,
|
|
||||||
small: true,
|
|
||||||
onPressed: _doExport),
|
|
||||||
DsButton('刷新',
|
|
||||||
icon: LucideIcons.refreshCw,
|
|
||||||
small: true,
|
|
||||||
onPressed: () =>
|
|
||||||
ref.read(stockInListProvider.notifier).reload()),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 桌面:筛选左侧 Wrap(避免 1280 溢出),重置推到最右(对齐原型 .sp 布局)。
|
// 桌面:筛选左侧 Wrap(避免 1280 溢出),重置推到最右(对齐原型 .sp 布局)。
|
||||||
|
// 窄屏不再走此工具栏(对齐移动原型:MSearchRow + 状态/详搜 sheet)。
|
||||||
return Row(
|
return Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
@@ -755,70 +921,9 @@ class _StockInListScreenState extends ConsumerState<StockInListScreen> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 操作按钮列表,表格与移动端卡片共用。入库特有的「打标签」通过 afterPrint 注入。
|
/// 入库单:窄屏卡片(原型 .m-card:图标徽章 + › 箭头;操作统一收进详情 sheet)。
|
||||||
List<Widget> _orderActions(BuildContext context, StockInOrder o) {
|
|
||||||
return buildOrderRowActions(
|
|
||||||
context: context,
|
|
||||||
readonly: WriteGuard.isReadonly(ref),
|
|
||||||
canWithdraw: ref.watch(isAdminProvider) ||
|
|
||||||
(o.operatorId != null &&
|
|
||||||
o.operatorId == ref.watch(currentUserIdProvider)),
|
|
||||||
status: o.status,
|
|
||||||
orderId: o.id,
|
|
||||||
onDetail: () => _showDetail(context, o.id),
|
|
||||||
onPrint: () async {
|
|
||||||
final order = await ref.read(stockInRepositoryProvider).get(o.id);
|
|
||||||
if (context.mounted) {
|
|
||||||
await showStockInOrderPrint(context, ref, order);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
afterPrint: [
|
|
||||||
TextButton(
|
|
||||||
onPressed: () async {
|
|
||||||
final order = await ref.read(stockInRepositoryProvider).get(o.id);
|
|
||||||
if (!context.mounted) return;
|
|
||||||
final shopInfo = ref.read(shopInfoProvider).valueOrNull;
|
|
||||||
final labels = order.items
|
|
||||||
.map((item) => LabelData(
|
|
||||||
productId: item.productId,
|
|
||||||
name: item.productName ?? '',
|
|
||||||
code: item.productCode ?? '',
|
|
||||||
series: item.productSeries,
|
|
||||||
spec: item.productSpec,
|
|
||||||
batchNo: item.batchNo,
|
|
||||||
productionDate: item.productionDate,
|
|
||||||
shopName: shopInfo?.name ?? '',
|
|
||||||
shopAddress: shopInfo?.address ?? '',
|
|
||||||
shopPhone: shopInfo?.phone ?? '',
|
|
||||||
))
|
|
||||||
.toList();
|
|
||||||
showAppDialog(
|
|
||||||
context: context,
|
|
||||||
builder: (_) => LabelPreviewDialog(
|
|
||||||
labels: labels,
|
|
||||||
qrFetcher: ref.read(productRepositoryProvider).getQRCodeBytes,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
child: Text('打标签',
|
|
||||||
style: TextStyle(fontSize: 12, color: context.tokens.primary)),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
onSettle: () => _confirmSettle(context, o.id, 'stock_in'),
|
|
||||||
onEdit: () => context.go('/stock-in/edit/${o.id}'),
|
|
||||||
onDelete: () => _confirmDelete(context, o),
|
|
||||||
onSubmit: () => _confirmSubmit(context, o),
|
|
||||||
onApprove: () => _confirmApprove(context, o),
|
|
||||||
onReject: () => _confirmReject(context, o),
|
|
||||||
onWithdraw: () => _confirmWithdraw(context, o),
|
|
||||||
// 入库退单:仅管理员/超管
|
|
||||||
canReturn: ref.watch(isAdminProvider),
|
|
||||||
onReturn: () => _confirmReturn(context, o),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 入库单:窄屏卡片
|
|
||||||
Widget _orderCard(BuildContext context, StockInOrder o) {
|
Widget _orderCard(BuildContext context, StockInOrder o) {
|
||||||
|
final hasPending = o.items.any((it) => it.costPrice == 0);
|
||||||
return MobileListCard(
|
return MobileListCard(
|
||||||
onTap: () => _showDetail(context, o.id),
|
onTap: () => _showDetail(context, o.id),
|
||||||
title: Text(o.orderNo,
|
title: Text(o.orderNo,
|
||||||
@@ -827,21 +932,30 @@ class _StockInListScreenState extends ConsumerState<StockInListScreen> {
|
|||||||
fontFamilyFallback: AppFonts.monoFallback,
|
fontFamilyFallback: AppFonts.monoFallback,
|
||||||
fontSize: 14)),
|
fontSize: 14)),
|
||||||
trailing: Row(mainAxisSize: MainAxisSize.min, children: [
|
trailing: Row(mainAxisSize: MainAxisSize.min, children: [
|
||||||
StatusBadge(_apiStatusToEnum(o.status)),
|
_icoBadge(_apiStatusToEnum(o.status).label),
|
||||||
if (returnStateBadge(context, o.returnState) != null) ...[
|
if (o.returnState == 'partial') ...[
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 5),
|
||||||
returnStateBadge(context, o.returnState)!,
|
_icoBadge('部分退单'),
|
||||||
|
] else if (o.returnState == 'full') ...[
|
||||||
|
const SizedBox(width: 5),
|
||||||
|
_icoBadge('已退单'),
|
||||||
],
|
],
|
||||||
|
if (hasPending) ...[
|
||||||
|
const SizedBox(width: 5),
|
||||||
|
_icoBadge('待定价'),
|
||||||
|
],
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
Icon(LucideIcons.chevronRight, size: 18, color: context.tokens.faint),
|
||||||
]),
|
]),
|
||||||
fields: [
|
fields: [
|
||||||
MobileCardField('供应商', o.partnerName ?? '-'),
|
MobileCardField('供应商', o.partnerName ?? '-'),
|
||||||
MobileCardField('仓库', o.warehouseName ?? '-'),
|
MobileCardField('仓库', o.warehouseName ?? '-'),
|
||||||
MobileCardField('合计金额',
|
MobileCardField('合计金额',
|
||||||
o.costTotal != null ? '¥${o.costTotal!.toStringAsFixed(2)}' : '-'),
|
o.costTotal != null ? '¥${o.costTotal!.toStringAsFixed(2)}' : '-'),
|
||||||
|
MobileCardField('入库时间', o.orderDate?.substring(0, 10) ?? '-'),
|
||||||
MobileCardField('入库员', o.operatorName ?? '-'),
|
MobileCardField('入库员', o.operatorName ?? '-'),
|
||||||
MobileCardField('审核员', o.reviewerName ?? '-'),
|
MobileCardField('审核员', o.reviewerName ?? '-'),
|
||||||
],
|
],
|
||||||
actions: _orderActions(context, o),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -989,17 +1103,20 @@ class _StockInListScreenState extends ConsumerState<StockInListScreen> {
|
|||||||
]));
|
]));
|
||||||
}
|
}
|
||||||
|
|
||||||
groups.add(DrawerActionGroup('打印', [
|
// 窄屏详情不出打印入口(移动端无打印——用户拍板)。
|
||||||
b('打印标签', () {
|
if (!context.isMobile) {
|
||||||
close();
|
groups.add(DrawerActionGroup('打印', [
|
||||||
_printLabels(o);
|
b('打印标签', () {
|
||||||
}),
|
close();
|
||||||
b('打印单据', () async {
|
_printLabels(o);
|
||||||
close();
|
}),
|
||||||
final order = await ref.read(stockInRepositoryProvider).get(o.id);
|
b('打印单据', () async {
|
||||||
if (mounted) await showStockInOrderPrint(context, ref, order);
|
close();
|
||||||
}),
|
final order = await ref.read(stockInRepositoryProvider).get(o.id);
|
||||||
]));
|
if (mounted) await showStockInOrderPrint(context, ref, order);
|
||||||
|
}),
|
||||||
|
]));
|
||||||
|
}
|
||||||
return groups;
|
return groups;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1038,52 +1155,102 @@ class _StockInListScreenState extends ConsumerState<StockInListScreen> {
|
|||||||
final controllers = {
|
final controllers = {
|
||||||
for (final it in pending) it.id!: TextEditingController()
|
for (final it in pending) it.id!: TextEditingController()
|
||||||
};
|
};
|
||||||
final ok = await showDialog<bool>(
|
final bool? ok;
|
||||||
context: context,
|
if (context.isMobile) {
|
||||||
builder: (ctx) => AlertDialog(
|
// 窄屏:底部 sheet(对齐原型 openConfirm)。
|
||||||
title: const Text('确认进价'),
|
ok = await showMSheet<bool>(
|
||||||
content: SizedBox(
|
context,
|
||||||
width: context.dialogWidth(440),
|
title: '确认进价',
|
||||||
child: SingleChildScrollView(
|
builder: (ctx) => Column(
|
||||||
child: Column(
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: pending.map((it) {
|
children: [
|
||||||
return Padding(
|
Text(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 6),
|
'调货「先卖后定价」:填写真实进价后,系统前向补齐库存成本、已出库成本快照,并按差额补一条应付流水——不反审核、不影响售价 / 应收。',
|
||||||
child: Row(
|
style: TextStyle(
|
||||||
children: [
|
fontSize: AppDims.fsSm, height: 1.6, color: ctx.tokens.muted),
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
'${it.productName ?? ''} ${it.productSpec ?? ''} ×${it.quantity.toStringAsFixed(0)}',
|
|
||||||
style: const TextStyle(fontSize: 13),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
SizedBox(
|
|
||||||
width: 120,
|
|
||||||
child: TextField(
|
|
||||||
controller: controllers[it.id],
|
|
||||||
keyboardType: const TextInputType.numberWithOptions(
|
|
||||||
decimal: true),
|
|
||||||
decoration: const InputDecoration(
|
|
||||||
prefixText: '¥', hintText: '进价', isDense: true),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}).toList(),
|
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(height: 14),
|
||||||
|
for (final it in pending)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 12),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'${it.productName ?? ''} ${it.productSpec ?? ''} · 数量 ${it.quantity.toStringAsFixed(0)}',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: AppDims.fsSm, color: ctx.tokens.muted)),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
TextField(
|
||||||
|
controller: controllers[it.id],
|
||||||
|
keyboardType:
|
||||||
|
const TextInputType.numberWithOptions(decimal: true),
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
prefixText: '¥',
|
||||||
|
hintText: '真实进价 0.00',
|
||||||
|
isDense: true),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
DsButton('取消', onPressed: () => Navigator.pop(ctx, false)),
|
DsButton('取消', onPressed: () => Navigator.of(context).pop(false)),
|
||||||
DsButton('确认',
|
DsButton('确认并补差额',
|
||||||
variant: DsBtnVariant.primary,
|
variant: DsBtnVariant.primary,
|
||||||
onPressed: () => Navigator.pop(ctx, true)),
|
onPressed: () => Navigator.of(context).pop(true)),
|
||||||
],
|
],
|
||||||
),
|
);
|
||||||
);
|
} else {
|
||||||
|
ok = await showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (ctx) => AlertDialog(
|
||||||
|
title: const Text('确认进价'),
|
||||||
|
content: SizedBox(
|
||||||
|
width: context.dialogWidth(440),
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: pending.map((it) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 6),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
'${it.productName ?? ''} ${it.productSpec ?? ''} ×${it.quantity.toStringAsFixed(0)}',
|
||||||
|
style: const TextStyle(fontSize: 13),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
SizedBox(
|
||||||
|
width: 120,
|
||||||
|
child: TextField(
|
||||||
|
controller: controllers[it.id],
|
||||||
|
keyboardType: const TextInputType.numberWithOptions(
|
||||||
|
decimal: true),
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
prefixText: '¥', hintText: '进价', isDense: true),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
DsButton('取消', onPressed: () => Navigator.pop(ctx, false)),
|
||||||
|
DsButton('确认',
|
||||||
|
variant: DsBtnVariant.primary,
|
||||||
|
onPressed: () => Navigator.pop(ctx, true)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
final items = <Map<String, dynamic>>[];
|
final items = <Map<String, dynamic>>[];
|
||||||
if (ok == true) {
|
if (ok == true) {
|
||||||
for (final it in pending) {
|
for (final it in pending) {
|
||||||
@@ -1792,3 +1959,315 @@ class _AdvSearchDialogState extends ConsumerState<_AdvSearchDialog> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── 详细搜索(窄屏底部 sheet,竖排单列;字段与桌面 _AdvSearchDialog 完全同集,
|
||||||
|
// 对齐原型 drawAdv;重置/应用按钮由 showMSheet actions 注入)──────────────
|
||||||
|
class _AdvSearchSheet extends ConsumerStatefulWidget {
|
||||||
|
final Map<String, String> initialDetail;
|
||||||
|
final String initialStatus;
|
||||||
|
final DateTimeRange? initialRange;
|
||||||
|
const _AdvSearchSheet({
|
||||||
|
super.key,
|
||||||
|
required this.initialDetail,
|
||||||
|
required this.initialStatus,
|
||||||
|
required this.initialRange,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
ConsumerState<_AdvSearchSheet> createState() => _AdvSearchSheetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AdvSearchSheetState extends ConsumerState<_AdvSearchSheet> {
|
||||||
|
late final TextEditingController _orderNoCtrl;
|
||||||
|
late final TextEditingController _productCtrl;
|
||||||
|
late final TextEditingController _productCodeCtrl;
|
||||||
|
late final TextEditingController _batchCtrl;
|
||||||
|
int? _partnerId;
|
||||||
|
String? _seriesName;
|
||||||
|
String? _specName;
|
||||||
|
int? _operatorId;
|
||||||
|
int? _reviewerId;
|
||||||
|
late String _status;
|
||||||
|
DateTimeRange? _range;
|
||||||
|
|
||||||
|
static const _statusCodes = ['', 'draft', 'pending', 'approved', 'rejected'];
|
||||||
|
static const _statusNames = ['全部', '草稿', '待审核', '已审核', '已拒绝'];
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
final d = widget.initialDetail;
|
||||||
|
_orderNoCtrl = TextEditingController(text: d['order_no'] ?? '');
|
||||||
|
_productCtrl = TextEditingController(text: d['product'] ?? '');
|
||||||
|
_productCodeCtrl = TextEditingController(text: d['product_code'] ?? '');
|
||||||
|
_batchCtrl = TextEditingController(text: d['batch'] ?? '');
|
||||||
|
_partnerId = int.tryParse(d['partner_id'] ?? '');
|
||||||
|
_seriesName = d['series'];
|
||||||
|
_specName = d['spec'];
|
||||||
|
_operatorId = int.tryParse(d['operator_id'] ?? '');
|
||||||
|
_reviewerId = int.tryParse(d['reviewer_id'] ?? '');
|
||||||
|
_status = widget.initialStatus;
|
||||||
|
_range = widget.initialRange;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_orderNoCtrl.dispose();
|
||||||
|
_productCtrl.dispose();
|
||||||
|
_productCodeCtrl.dispose();
|
||||||
|
_batchCtrl.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 重置(sheet actions「重置」调用)。
|
||||||
|
void reset() {
|
||||||
|
setState(() {
|
||||||
|
_orderNoCtrl.clear();
|
||||||
|
_productCtrl.clear();
|
||||||
|
_productCodeCtrl.clear();
|
||||||
|
_batchCtrl.clear();
|
||||||
|
_partnerId = null;
|
||||||
|
_seriesName = null;
|
||||||
|
_specName = null;
|
||||||
|
_operatorId = null;
|
||||||
|
_reviewerId = null;
|
||||||
|
_status = '';
|
||||||
|
_range = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 应用(sheet actions「应用」调用):组装结果并关闭 sheet。
|
||||||
|
void apply() {
|
||||||
|
final d = <String, String>{};
|
||||||
|
void put(String k, String? v) {
|
||||||
|
if (v != null && v.trim().isNotEmpty) d[k] = v.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
put('order_no', _orderNoCtrl.text);
|
||||||
|
put('partner_id', _partnerId?.toString());
|
||||||
|
put('product', _productCtrl.text);
|
||||||
|
put('product_code', _productCodeCtrl.text);
|
||||||
|
put('series', _seriesName);
|
||||||
|
put('spec', _specName);
|
||||||
|
put('batch', _batchCtrl.text);
|
||||||
|
put('operator_id', _operatorId?.toString());
|
||||||
|
put('reviewer_id', _reviewerId?.toString());
|
||||||
|
Navigator.of(context).pop(_AdvResult(d, _status, _range));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _pickDate() async {
|
||||||
|
final range = await showWheelDateRange(context, initial: _range);
|
||||||
|
if (range != null) setState(() => _range = range);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<OptionItem> _supplierOpts() =>
|
||||||
|
ref
|
||||||
|
.watch(allPartnersProvider)
|
||||||
|
.valueOrNull
|
||||||
|
?.data
|
||||||
|
.map((p) => OptionItem(id: p.id, name: p.name))
|
||||||
|
.toList() ??
|
||||||
|
const [];
|
||||||
|
|
||||||
|
List<OptionItem> _seriesOpts() =>
|
||||||
|
ref
|
||||||
|
.watch(productSeriesListProvider)
|
||||||
|
.valueOrNull
|
||||||
|
?.map((s) => OptionItem(id: s.id, name: s.name))
|
||||||
|
.toList() ??
|
||||||
|
const [];
|
||||||
|
|
||||||
|
List<OptionItem> _specOpts() =>
|
||||||
|
ref
|
||||||
|
.watch(productSpecListProvider)
|
||||||
|
.valueOrNull
|
||||||
|
?.map((s) => OptionItem(id: s.id, name: s.name))
|
||||||
|
.toList() ??
|
||||||
|
const [];
|
||||||
|
|
||||||
|
List<OptionItem> _userOpts() =>
|
||||||
|
ref
|
||||||
|
.watch(userListProvider)
|
||||||
|
.valueOrNull
|
||||||
|
?.map((u) => OptionItem(
|
||||||
|
id: u.id,
|
||||||
|
name: (u.realName != null && u.realName!.isNotEmpty)
|
||||||
|
? u.realName!
|
||||||
|
: u.username))
|
||||||
|
.toList() ??
|
||||||
|
const [];
|
||||||
|
|
||||||
|
int? _idByName(List<OptionItem> opts, String? name) {
|
||||||
|
if (name == null || name.isEmpty) return null;
|
||||||
|
return opts.where((o) => o.name == name).map((o) => o.id).firstOrNull;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final t = context.tokens;
|
||||||
|
final seriesOpts = _seriesOpts();
|
||||||
|
final specOpts = _specOpts();
|
||||||
|
|
||||||
|
// 原型 .m-field:label + 控件竖排,单列。
|
||||||
|
Widget field(String label, Widget child) => Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 12),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text(label,
|
||||||
|
style: TextStyle(fontSize: AppDims.fsSm, color: t.muted)),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
child,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// 原型 .m-input:h42 带框文本输入。
|
||||||
|
Widget textField(TextEditingController c, String hint) => Container(
|
||||||
|
height: 42,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 11),
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: t.surface,
|
||||||
|
border: Border.all(color: t.border),
|
||||||
|
borderRadius: BorderRadius.circular(AppDims.rMd),
|
||||||
|
),
|
||||||
|
child: TextField(
|
||||||
|
controller: c,
|
||||||
|
style: TextStyle(fontSize: AppDims.fsBody, color: t.text),
|
||||||
|
decoration: InputDecoration(
|
||||||
|
isCollapsed: true,
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
|
border: InputBorder.none,
|
||||||
|
filled: false,
|
||||||
|
enabledBorder: InputBorder.none,
|
||||||
|
focusedBorder: InputBorder.none,
|
||||||
|
hintText: hint,
|
||||||
|
hintStyle: TextStyle(color: t.faint, fontSize: AppDims.fsBody),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget combo({
|
||||||
|
required List<OptionItem> opts,
|
||||||
|
required int? selectedId,
|
||||||
|
required String hint,
|
||||||
|
required ValueChanged<int?> onChanged,
|
||||||
|
}) =>
|
||||||
|
ComboSearchField(
|
||||||
|
options: opts,
|
||||||
|
selectedId: selectedId,
|
||||||
|
hint: hint,
|
||||||
|
onChanged: onChanged,
|
||||||
|
height: 42,
|
||||||
|
fillColor: t.surface,
|
||||||
|
);
|
||||||
|
|
||||||
|
final statusItems = [
|
||||||
|
for (var i = 0; i < _statusNames.length; i++)
|
||||||
|
OptionItem(id: i, name: _statusNames[i]),
|
||||||
|
];
|
||||||
|
final statusSelIdx = _statusCodes.indexOf(_status);
|
||||||
|
|
||||||
|
final dateHas = _range != null;
|
||||||
|
final dateField = InkWell(
|
||||||
|
onTap: _pickDate,
|
||||||
|
borderRadius: BorderRadius.circular(AppDims.rMd),
|
||||||
|
child: Container(
|
||||||
|
height: 42,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 11),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: t.surface,
|
||||||
|
border: Border.all(color: t.border),
|
||||||
|
borderRadius: BorderRadius.circular(AppDims.rMd),
|
||||||
|
),
|
||||||
|
child: Row(children: [
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
dateHas
|
||||||
|
? '${_range!.start.toString().substring(0, 10)} ~ ${_range!.end.toString().substring(0, 10)}'
|
||||||
|
: '全部时间',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: AppDims.fsBody, color: dateHas ? t.text : t.faint),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (dateHas)
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () => setState(() => _range = null),
|
||||||
|
child: Icon(LucideIcons.x, size: 14, color: t.muted),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
Icon(LucideIcons.calendar, size: 16, color: t.faint),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
field('单据编号', textField(_orderNoCtrl, '如 RK20260401001')),
|
||||||
|
field(
|
||||||
|
'供应商',
|
||||||
|
combo(
|
||||||
|
opts: _supplierOpts(),
|
||||||
|
selectedId: _partnerId,
|
||||||
|
hint: '全部供应商',
|
||||||
|
onChanged: (v) => setState(() => _partnerId = v))),
|
||||||
|
field('商品名', textField(_productCtrl, '输入酒名')),
|
||||||
|
field('商品编码', textField(_productCodeCtrl, '输入商品编码')),
|
||||||
|
field(
|
||||||
|
'系列',
|
||||||
|
combo(
|
||||||
|
opts: seriesOpts,
|
||||||
|
selectedId: _idByName(seriesOpts, _seriesName),
|
||||||
|
hint: '全部系列',
|
||||||
|
onChanged: (v) => setState(() => _seriesName = v == null
|
||||||
|
? null
|
||||||
|
: seriesOpts
|
||||||
|
.where((o) => o.id == v)
|
||||||
|
.map((o) => o.name)
|
||||||
|
.firstOrNull))),
|
||||||
|
field(
|
||||||
|
'规格',
|
||||||
|
combo(
|
||||||
|
opts: specOpts,
|
||||||
|
selectedId: _idByName(specOpts, _specName),
|
||||||
|
hint: '全部规格',
|
||||||
|
onChanged: (v) => setState(() => _specName = v == null
|
||||||
|
? null
|
||||||
|
: specOpts
|
||||||
|
.where((o) => o.id == v)
|
||||||
|
.map((o) => o.name)
|
||||||
|
.firstOrNull))),
|
||||||
|
field('批次号', textField(_batchCtrl, '输入批次号')),
|
||||||
|
field(
|
||||||
|
'状态',
|
||||||
|
ComboSearchField(
|
||||||
|
options: statusItems,
|
||||||
|
selectedId: statusSelIdx <= 0 ? null : statusSelIdx,
|
||||||
|
hint: '全部',
|
||||||
|
height: 42,
|
||||||
|
fillColor: t.surface,
|
||||||
|
onChanged: (id) => setState(
|
||||||
|
() => _status = (id == null) ? '' : _statusCodes[id]),
|
||||||
|
)),
|
||||||
|
field(
|
||||||
|
'入库员',
|
||||||
|
combo(
|
||||||
|
opts: _userOpts(),
|
||||||
|
selectedId: _operatorId,
|
||||||
|
hint: '全部入库员',
|
||||||
|
onChanged: (v) => setState(() => _operatorId = v))),
|
||||||
|
field(
|
||||||
|
'审核人',
|
||||||
|
combo(
|
||||||
|
opts: _userOpts(),
|
||||||
|
selectedId: _reviewerId,
|
||||||
|
hint: '全部审核人',
|
||||||
|
onChanged: (v) => setState(() => _reviewerId = v))),
|
||||||
|
field('单据日期', dateField),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import '../../core/utils/print_util.dart' show LabelData;
|
|||||||
import '../../core/auth/auth_state.dart'
|
import '../../core/auth/auth_state.dart'
|
||||||
show isAdminProvider, currentUserIdProvider;
|
show isAdminProvider, currentUserIdProvider;
|
||||||
import '../../models/stock_out.dart';
|
import '../../models/stock_out.dart';
|
||||||
|
import '../../models/stock_summary.dart';
|
||||||
import '../../providers/stock_out_provider.dart';
|
import '../../providers/stock_out_provider.dart';
|
||||||
import '../../providers/finance_provider.dart'
|
import '../../providers/finance_provider.dart'
|
||||||
show financeRepositoryProvider, financePartnerRowsProvider;
|
show financeRepositoryProvider, financePartnerRowsProvider;
|
||||||
@@ -30,12 +31,15 @@ import '../../widgets/order_detail_drawer.dart';
|
|||||||
import '../../widgets/ds/ds_atoms.dart';
|
import '../../widgets/ds/ds_atoms.dart';
|
||||||
import '../../widgets/ds/ds_kpi.dart';
|
import '../../widgets/ds/ds_kpi.dart';
|
||||||
import '../../widgets/ds/ds_table.dart';
|
import '../../widgets/ds/ds_table.dart';
|
||||||
|
import '../../widgets/ds/m_kpi_grid.dart';
|
||||||
|
import '../../widgets/ds/m_search_row.dart';
|
||||||
|
import '../../widgets/ds/m_sheet.dart';
|
||||||
|
import '../../widgets/ds/status_icon_map.dart';
|
||||||
import '../../widgets/mobile_list_card.dart';
|
import '../../widgets/mobile_list_card.dart';
|
||||||
import '../../widgets/status_badge.dart';
|
import '../../widgets/status_badge.dart';
|
||||||
import '../../widgets/searchable_option_field.dart';
|
import '../../widgets/searchable_option_field.dart';
|
||||||
import '../../widgets/combo_search_field.dart';
|
import '../../widgets/combo_search_field.dart';
|
||||||
import '../../widgets/order_print_preview_dialog.dart';
|
import '../../widgets/order_print_preview_dialog.dart';
|
||||||
import '../../widgets/order_row_actions.dart';
|
|
||||||
import '../../widgets/order_return_dialog.dart';
|
import '../../widgets/order_return_dialog.dart';
|
||||||
import '../../widgets/wheel_date_picker.dart';
|
import '../../widgets/wheel_date_picker.dart';
|
||||||
import '../../widgets/write_guard.dart';
|
import '../../widgets/write_guard.dart';
|
||||||
@@ -324,16 +328,25 @@ class _StockOutListScreenState extends ConsumerState<StockOutListScreen> {
|
|||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final mobile = context.isMobile;
|
||||||
return Container(
|
return Container(
|
||||||
color: context.tokens.bg,
|
color: context.tokens.bg,
|
||||||
padding: context.isMobile
|
padding: mobile
|
||||||
? EdgeInsets.zero
|
? EdgeInsets.zero
|
||||||
// 原型 .main{padding:22px 26px}
|
// 原型 .main{padding:22px 26px}
|
||||||
: const EdgeInsets.fromLTRB(26, 22, 26, 22),
|
: const EdgeInsets.fromLTRB(26, 22, 26, 22),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
_buildHeader(total),
|
// 窄屏对齐移动原型 m-stock-out-list:标题在顶栏、正文从 KPI 起;
|
||||||
_buildKpis(),
|
// 无页头按钮(移动端无建单/导出/打印入口——用户拍板)。
|
||||||
|
if (!mobile) ...[
|
||||||
|
_buildHeader(total),
|
||||||
|
_buildKpis(),
|
||||||
|
] else ...[
|
||||||
|
_buildMobileKpis(total),
|
||||||
|
_buildMobileSearchRow(),
|
||||||
|
_buildMobileSection(total),
|
||||||
|
],
|
||||||
Expanded(
|
Expanded(
|
||||||
child: DsTable(
|
child: DsTable(
|
||||||
total: total,
|
total: total,
|
||||||
@@ -343,7 +356,7 @@ class _StockOutListScreenState extends ConsumerState<StockOutListScreen> {
|
|||||||
ref.read(stockOutListProvider.notifier).setPage(p),
|
ref.read(stockOutListProvider.notifier).setPage(p),
|
||||||
onPageSizeChanged: (s) =>
|
onPageSizeChanged: (s) =>
|
||||||
ref.read(stockOutListProvider.notifier).setPageSize(s),
|
ref.read(stockOutListProvider.notifier).setPageSize(s),
|
||||||
toolbar: _buildToolbar(orders),
|
toolbar: mobile ? null : _buildToolbar(orders),
|
||||||
emptyText: '没有匹配的出库单 · 试试调整筛选或搜索',
|
emptyText: '没有匹配的出库单 · 试试调整筛选或搜索',
|
||||||
columns: const [
|
columns: const [
|
||||||
DsColumn('order_no', '单号'),
|
DsColumn('order_no', '单号'),
|
||||||
@@ -571,13 +584,215 @@ class _StockOutListScreenState extends ConsumerState<StockOutListScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── 窄屏形态(对齐移动原型 m-stock-out-list.html)─────────────────
|
||||||
|
|
||||||
|
/// 状态设置(主状态 / ret:退单态互斥),KPI 卡与状态 sheet 共用。
|
||||||
|
void _setStatus(String code) {
|
||||||
|
final n = ref.read(stockOutListProvider.notifier);
|
||||||
|
if (code.startsWith('ret:')) {
|
||||||
|
setState(() {
|
||||||
|
_statusFilter = code;
|
||||||
|
_returnState = code.substring(4);
|
||||||
|
});
|
||||||
|
n.setStatus('');
|
||||||
|
_applyDetail();
|
||||||
|
} else {
|
||||||
|
setState(() {
|
||||||
|
_statusFilter = code;
|
||||||
|
_returnState = '';
|
||||||
|
});
|
||||||
|
n.setStatus(code);
|
||||||
|
_applyDetail();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 状态词 → (前景, 软底):镜像原型 .badge.b-*,全走 token。
|
||||||
|
(Color, Color) _statusColors(String label) {
|
||||||
|
final t = context.tokens;
|
||||||
|
return switch (label) {
|
||||||
|
'待审核' => (t.warn, t.warnBg),
|
||||||
|
'已审核' => (t.success, t.successBg),
|
||||||
|
'已拒绝' => (t.danger, t.dangerBg),
|
||||||
|
'部分退单' => (t.warn, t.warnBg),
|
||||||
|
'已退单' => (t.danger, t.dangerBg),
|
||||||
|
'待定价' => (t.warn, t.warnBg),
|
||||||
|
_ => (t.muted, t.infoSoft), // 草稿
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 原型 .badge.ico:纯图标小徽章(卡片右上态标 / 状态 sheet 选项行)。
|
||||||
|
Widget _icoBadge(String label) {
|
||||||
|
final (fg, bg) = _statusColors(label);
|
||||||
|
return Container(
|
||||||
|
height: 22,
|
||||||
|
width: 26,
|
||||||
|
alignment: Alignment.center,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: bg,
|
||||||
|
borderRadius: BorderRadius.circular(AppDims.rPill),
|
||||||
|
),
|
||||||
|
child: Icon(statusIcon(label), size: 12, color: fg),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 原型 .m-kpi 2×2:近30天笔数/金额 + 可点筛选的待审核/草稿(再点取消)。
|
||||||
|
Widget _buildMobileKpis(int total) {
|
||||||
|
final StockSummary? summary =
|
||||||
|
ref.watch(stockOutSummary30Provider).valueOrNull;
|
||||||
|
String pct(double? p) => p == null
|
||||||
|
? '较上期 —'
|
||||||
|
: '${p >= 0 ? '▲' : '▼'} ${p.abs().toStringAsFixed(1)}% 较上期';
|
||||||
|
MKpiDeltaTone tone(double? p) => p == null
|
||||||
|
? MKpiDeltaTone.normal
|
||||||
|
: (p >= 0 ? MKpiDeltaTone.up : MKpiDeltaTone.down);
|
||||||
|
final pendingCount = summary?.pendingCount ?? 0;
|
||||||
|
final draftCount = summary?.draftCount ?? 0;
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(12, 12, 12, 0),
|
||||||
|
child: MKpiGrid(items: [
|
||||||
|
MKpiItem(
|
||||||
|
label: '近30天出库笔数',
|
||||||
|
value: NumberFormat.decimalPattern()
|
||||||
|
.format(summary?.monthCount ?? total),
|
||||||
|
icon: LucideIcons.upload,
|
||||||
|
delta: pct(summary?.countDeltaPct),
|
||||||
|
deltaTone: tone(summary?.countDeltaPct)),
|
||||||
|
MKpiItem(
|
||||||
|
label: '近30天出库金额',
|
||||||
|
value: summary != null ? _yuanWan(summary.monthAmount) : '—',
|
||||||
|
icon: LucideIcons.banknote,
|
||||||
|
delta: pct(summary?.amountDeltaPct),
|
||||||
|
deltaTone: tone(summary?.amountDeltaPct)),
|
||||||
|
MKpiItem(
|
||||||
|
label: '待审核 · 点击筛选',
|
||||||
|
value: '$pendingCount',
|
||||||
|
icon: LucideIcons.clock,
|
||||||
|
delta: pendingCount > 0 ? '需尽快处理' : '点击筛选',
|
||||||
|
deltaTone:
|
||||||
|
pendingCount > 0 ? MKpiDeltaTone.warn : MKpiDeltaTone.normal,
|
||||||
|
selected: _statusFilter == 'pending',
|
||||||
|
onTap: () =>
|
||||||
|
_setStatus(_statusFilter == 'pending' ? '' : 'pending')),
|
||||||
|
MKpiItem(
|
||||||
|
label: '草稿 · 点击筛选',
|
||||||
|
value: '$draftCount',
|
||||||
|
icon: LucideIcons.fileText,
|
||||||
|
delta: draftCount > 0 ? '待提交审核' : '点击筛选',
|
||||||
|
selected: _statusFilter == 'draft',
|
||||||
|
onTap: () => _setStatus(_statusFilter == 'draft' ? '' : 'draft')),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 详细搜索是否有生效条件(不含状态钮自己的态)→ 详搜钮高亮。
|
||||||
|
bool get _mobileAdvActive =>
|
||||||
|
_orderNo.isNotEmpty ||
|
||||||
|
_productInfo.isNotEmpty ||
|
||||||
|
_productCode.isNotEmpty ||
|
||||||
|
_batch.isNotEmpty ||
|
||||||
|
_series.isNotEmpty ||
|
||||||
|
_spec.isNotEmpty ||
|
||||||
|
_partnerId != null ||
|
||||||
|
_operatorId != null ||
|
||||||
|
_reviewerId != null ||
|
||||||
|
_dateRange != null;
|
||||||
|
|
||||||
|
/// 原型搜索区:搜索框 + 纯文字状态钮 + 图标详搜钮。
|
||||||
|
Widget _buildMobileSearchRow() {
|
||||||
|
final statusLabel = _statusFilter.isEmpty
|
||||||
|
? '全部'
|
||||||
|
: _statusOptions
|
||||||
|
.firstWhere((e) => e.$1 == _statusFilter, orElse: () => ('', '全部'))
|
||||||
|
.$2;
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(12, 10, 12, 0),
|
||||||
|
child: MSearchRow(
|
||||||
|
controller: _searchCtrl,
|
||||||
|
hint: '搜索单号 / 客户 / 酒名',
|
||||||
|
// setKeyword 内置 350ms 防抖 + 同词去重,oninput 直连即可(对齐原型)。
|
||||||
|
onChanged: (v) => ref.read(stockOutListProvider.notifier).setKeyword(v),
|
||||||
|
onSubmitted: (v) =>
|
||||||
|
ref.read(stockOutListProvider.notifier).setKeyword(v),
|
||||||
|
statusLabel: statusLabel,
|
||||||
|
statusActive: _statusFilter.isNotEmpty,
|
||||||
|
onStatusTap: _openStatusSheet,
|
||||||
|
filterActive: _mobileAdvActive,
|
||||||
|
onFilterTap: _openAdvSearch,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 原型 .m-section:出库单 · 共 N。
|
||||||
|
Widget _buildMobileSection(int total) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(14, 14, 14, 8),
|
||||||
|
child: Align(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
child: Text('出库单 · 共 $total',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: AppDims.fsSm,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
letterSpacing: .4,
|
||||||
|
color: context.tokens.muted)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 状态筛选底部 sheet(原型 openStatusSheet 的 m-opt 列表:图标 + 状态词 + 勾)。
|
||||||
|
Future<void> _openStatusSheet() async {
|
||||||
|
final sel = await showMSheet<String>(
|
||||||
|
context,
|
||||||
|
title: '单据状态',
|
||||||
|
builder: (ctx) {
|
||||||
|
final t = ctx.tokens;
|
||||||
|
return Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
for (var i = 0; i < _statusOptions.length; i++)
|
||||||
|
InkWell(
|
||||||
|
onTap: () => Navigator.of(ctx).pop(_statusOptions[i].$1),
|
||||||
|
child: Container(
|
||||||
|
padding:
|
||||||
|
const EdgeInsets.symmetric(vertical: 13, horizontal: 4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: i < _statusOptions.length - 1
|
||||||
|
? Border(bottom: BorderSide(color: t.borderSubtle))
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
child: Row(children: [
|
||||||
|
if (_statusOptions[i].$2 != '全部') ...[
|
||||||
|
_icoBadge(_statusOptions[i].$2),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
],
|
||||||
|
Text(_statusOptions[i].$2,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: AppDims.fsBody,
|
||||||
|
fontWeight: _statusOptions[i].$1 == _statusFilter
|
||||||
|
? FontWeight.w600
|
||||||
|
: FontWeight.w400,
|
||||||
|
color: _statusOptions[i].$1 == _statusFilter
|
||||||
|
? t.primary
|
||||||
|
: t.text)),
|
||||||
|
const Spacer(),
|
||||||
|
if (_statusOptions[i].$1 == _statusFilter)
|
||||||
|
Icon(LucideIcons.check, size: 18, color: t.primary),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (sel == null || !mounted) return;
|
||||||
|
_setStatus(sel);
|
||||||
|
}
|
||||||
|
|
||||||
// ── 工具栏(原型 .toolbar)─────────────────────────────────────
|
// ── 工具栏(原型 .toolbar)─────────────────────────────────────
|
||||||
|
|
||||||
Widget _buildToolbar(List<StockOutOrder> orders) {
|
Widget _buildToolbar(List<StockOutOrder> orders) {
|
||||||
final isMobile = context.isMobile;
|
// 仅桌面调用(窄屏走 MSearchRow + 状态/详搜 sheet,不再进此工具栏)。
|
||||||
|
|
||||||
final searchField = SizedBox(
|
final searchField = SizedBox(
|
||||||
width: isMobile ? double.infinity : 260,
|
width: 260,
|
||||||
child: DsSearchBox(
|
child: DsSearchBox(
|
||||||
controller: _searchCtrl,
|
controller: _searchCtrl,
|
||||||
hint: '单号 / 酒名 / 商品编码',
|
hint: '单号 / 酒名 / 商品编码',
|
||||||
@@ -597,7 +812,7 @@ class _StockOutListScreenState extends ConsumerState<StockOutListScreen> {
|
|||||||
options: customers,
|
options: customers,
|
||||||
selectedId: _partnerId,
|
selectedId: _partnerId,
|
||||||
hint: '客户',
|
hint: '客户',
|
||||||
width: isMobile ? 320 : 180,
|
width: 180,
|
||||||
onChanged: (id) {
|
onChanged: (id) {
|
||||||
setState(() => _partnerId = id);
|
setState(() => _partnerId = id);
|
||||||
_applyDetail();
|
_applyDetail();
|
||||||
@@ -680,33 +895,15 @@ class _StockOutListScreenState extends ConsumerState<StockOutListScreen> {
|
|||||||
onPressed: _resetFilters,
|
onPressed: _resetFilters,
|
||||||
);
|
);
|
||||||
|
|
||||||
// 筛选 chip(不含重置)——重置在桌面推到最右、移动端并入 Wrap 末尾(对齐原型 .sp)。
|
// 筛选 chip(不含重置)——重置推到最右(对齐原型 .sp)。
|
||||||
final filterChips = <Widget>[
|
final filterChips = <Widget>[
|
||||||
if (!isMobile) customerField,
|
customerField,
|
||||||
warehouseChip,
|
warehouseChip,
|
||||||
statusChip,
|
statusChip,
|
||||||
dateChip,
|
dateChip,
|
||||||
advBtn,
|
advBtn,
|
||||||
];
|
];
|
||||||
|
|
||||||
if (isMobile) {
|
|
||||||
return Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
searchField,
|
|
||||||
const SizedBox(height: AppDims.sp2),
|
|
||||||
customerField,
|
|
||||||
const SizedBox(height: AppDims.sp2),
|
|
||||||
Wrap(
|
|
||||||
spacing: AppDims.sp2,
|
|
||||||
runSpacing: AppDims.sp2,
|
|
||||||
crossAxisAlignment: WrapCrossAlignment.center,
|
|
||||||
children: [...filterChips, resetBtn],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Row(
|
return Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
@@ -744,75 +941,63 @@ class _StockOutListScreenState extends ConsumerState<StockOutListScreen> {
|
|||||||
// ── 详细搜索模态(11 字段)──────────────────────────────────────
|
// ── 详细搜索模态(11 字段)──────────────────────────────────────
|
||||||
|
|
||||||
Future<void> _openAdvSearch() async {
|
Future<void> _openAdvSearch() async {
|
||||||
final result = await showAppDialog<_AdvResult>(
|
final initial = _AdvResult(
|
||||||
context: context,
|
orderNo: _orderNo,
|
||||||
builder: (_) => _AdvancedSearchDialog(
|
productInfo: _productInfo,
|
||||||
initial: _AdvResult(
|
productCode: _productCode,
|
||||||
orderNo: _orderNo,
|
batch: _batch,
|
||||||
productInfo: _productInfo,
|
partnerId: _partnerId,
|
||||||
productCode: _productCode,
|
series: _series,
|
||||||
batch: _batch,
|
spec: _spec,
|
||||||
partnerId: _partnerId,
|
operatorId: _operatorId,
|
||||||
series: _series,
|
reviewerId: _reviewerId,
|
||||||
spec: _spec,
|
status: _statusFilter,
|
||||||
operatorId: _operatorId,
|
dateRange: _dateRange,
|
||||||
reviewerId: _reviewerId,
|
|
||||||
status: _statusFilter,
|
|
||||||
dateRange: _dateRange,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
if (result == null || !mounted) return;
|
final _AdvResult? result;
|
||||||
|
if (context.isMobile) {
|
||||||
|
// 窄屏:底部 sheet 竖排单列(字段与桌面同集,原型 drawAdv)。
|
||||||
|
final formKey = GlobalKey<_AdvSearchSheetState>();
|
||||||
|
result = await showMSheet<_AdvResult>(
|
||||||
|
context,
|
||||||
|
title: '详细搜索',
|
||||||
|
builder: (_) => _AdvSearchSheet(key: formKey, initial: initial),
|
||||||
|
actions: [
|
||||||
|
DsButton('重置', onPressed: () => formKey.currentState?.reset()),
|
||||||
|
DsButton('应用',
|
||||||
|
variant: DsBtnVariant.primary,
|
||||||
|
onPressed: () => formKey.currentState?.apply()),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
result = await showAppDialog<_AdvResult>(
|
||||||
|
context: context,
|
||||||
|
builder: (_) => _AdvancedSearchDialog(initial: initial),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
final r = result;
|
||||||
|
if (r == null || !mounted) return;
|
||||||
setState(() {
|
setState(() {
|
||||||
_orderNo = result.orderNo;
|
_orderNo = r.orderNo;
|
||||||
_productInfo = result.productInfo;
|
_productInfo = r.productInfo;
|
||||||
_productCode = result.productCode;
|
_productCode = r.productCode;
|
||||||
_batch = result.batch;
|
_batch = r.batch;
|
||||||
_partnerId = result.partnerId;
|
_partnerId = r.partnerId;
|
||||||
_series = result.series;
|
_series = r.series;
|
||||||
_spec = result.spec;
|
_spec = r.spec;
|
||||||
_operatorId = result.operatorId;
|
_operatorId = r.operatorId;
|
||||||
_reviewerId = result.reviewerId;
|
_reviewerId = r.reviewerId;
|
||||||
_statusFilter = result.status;
|
_statusFilter = r.status;
|
||||||
_dateRange = result.dateRange;
|
_dateRange = r.dateRange;
|
||||||
});
|
});
|
||||||
_applyAll();
|
_applyAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── 行操作 / 卡片 ─────────────────────────────────────────────
|
// ── 窄屏卡片 ─────────────────────────────────────────────────
|
||||||
|
|
||||||
List<Widget> _orderActions(BuildContext context, StockOutOrder o) {
|
|
||||||
return buildOrderRowActions(
|
|
||||||
context: context,
|
|
||||||
readonly: WriteGuard.isReadonly(ref),
|
|
||||||
canWithdraw: ref.watch(isAdminProvider) ||
|
|
||||||
(o.operatorId != null &&
|
|
||||||
o.operatorId == ref.watch(currentUserIdProvider)),
|
|
||||||
status: o.status,
|
|
||||||
orderId: o.id,
|
|
||||||
onDetail: () => _showDetail(context, o.id),
|
|
||||||
onPrint: () async {
|
|
||||||
final order = await ref.read(stockOutRepositoryProvider).get(o.id);
|
|
||||||
if (context.mounted) {
|
|
||||||
await showStockOutOrderPrint(context, ref, order);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onSettle: () => _confirmSettle(context, o.id, 'stock_out'),
|
|
||||||
onEdit: () => context.go('/stock-out/edit/${o.id}'),
|
|
||||||
onDelete: () => _confirmDelete(context, o),
|
|
||||||
onSubmit: () => _confirmSubmit(context, o),
|
|
||||||
onApprove: () => _confirmApprove(context, o),
|
|
||||||
onReject: () => _confirmReject(context, o),
|
|
||||||
onWithdraw: () => _confirmWithdraw(context, o),
|
|
||||||
// 出库退单:管理员/超管 或 本人单
|
|
||||||
canReturn: ref.watch(isAdminProvider) ||
|
|
||||||
(o.operatorId != null &&
|
|
||||||
o.operatorId == ref.watch(currentUserIdProvider)),
|
|
||||||
onReturn: () => _confirmReturn(context, o),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
/// 出库单:窄屏卡片(原型 .m-card:图标徽章 + › 箭头;操作统一收进详情 sheet)。
|
||||||
Widget _orderCard(BuildContext context, StockOutOrder o) {
|
Widget _orderCard(BuildContext context, StockOutOrder o) {
|
||||||
|
final hasPending = o.items.any((it) => it.salePrice <= 0);
|
||||||
return MobileListCard(
|
return MobileListCard(
|
||||||
onTap: () => _showDetail(context, o.id),
|
onTap: () => _showDetail(context, o.id),
|
||||||
title: Text(o.orderNo,
|
title: Text(o.orderNo,
|
||||||
@@ -820,7 +1005,22 @@ class _StockOutListScreenState extends ConsumerState<StockOutListScreen> {
|
|||||||
fontFamily: AppFonts.mono,
|
fontFamily: AppFonts.mono,
|
||||||
fontFamilyFallback: AppFonts.monoFallback,
|
fontFamilyFallback: AppFonts.monoFallback,
|
||||||
fontSize: 14)),
|
fontSize: 14)),
|
||||||
trailing: _cellStatus(o),
|
trailing: Row(mainAxisSize: MainAxisSize.min, children: [
|
||||||
|
_icoBadge(_apiStatusToEnum(o.status).label),
|
||||||
|
if (o.returnState == 'partial') ...[
|
||||||
|
const SizedBox(width: 5),
|
||||||
|
_icoBadge('部分退单'),
|
||||||
|
] else if (o.returnState == 'full') ...[
|
||||||
|
const SizedBox(width: 5),
|
||||||
|
_icoBadge('已退单'),
|
||||||
|
],
|
||||||
|
if (hasPending) ...[
|
||||||
|
const SizedBox(width: 5),
|
||||||
|
_icoBadge('待定价'),
|
||||||
|
],
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
Icon(LucideIcons.chevronRight, size: 18, color: context.tokens.faint),
|
||||||
|
]),
|
||||||
fields: [
|
fields: [
|
||||||
MobileCardField('客户', o.partnerName ?? '-'),
|
MobileCardField('客户', o.partnerName ?? '-'),
|
||||||
MobileCardField('仓库', o.warehouseName ?? '-'),
|
MobileCardField('仓库', o.warehouseName ?? '-'),
|
||||||
@@ -828,7 +1028,6 @@ class _StockOutListScreenState extends ConsumerState<StockOutListScreen> {
|
|||||||
o.saleTotal != null ? '¥${o.saleTotal!.toStringAsFixed(2)}' : '-'),
|
o.saleTotal != null ? '¥${o.saleTotal!.toStringAsFixed(2)}' : '-'),
|
||||||
MobileCardField('出库时间', o.orderDate?.substring(0, 10) ?? '-'),
|
MobileCardField('出库时间', o.orderDate?.substring(0, 10) ?? '-'),
|
||||||
],
|
],
|
||||||
actions: _orderActions(context, o),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -984,17 +1183,20 @@ class _StockOutListScreenState extends ConsumerState<StockOutListScreen> {
|
|||||||
]));
|
]));
|
||||||
}
|
}
|
||||||
|
|
||||||
groups.add(DrawerActionGroup('打印', [
|
// 窄屏详情不出打印入口(移动端无打印——用户拍板)。
|
||||||
b('打印标签', () {
|
if (!context.isMobile) {
|
||||||
close();
|
groups.add(DrawerActionGroup('打印', [
|
||||||
_printLabels(o);
|
b('打印标签', () {
|
||||||
}),
|
close();
|
||||||
b('打印单据', () async {
|
_printLabels(o);
|
||||||
close();
|
}),
|
||||||
final order = await ref.read(stockOutRepositoryProvider).get(o.id);
|
b('打印单据', () async {
|
||||||
if (mounted) await showStockOutOrderPrint(context, ref, order);
|
close();
|
||||||
}),
|
final order = await ref.read(stockOutRepositoryProvider).get(o.id);
|
||||||
]));
|
if (mounted) await showStockOutOrderPrint(context, ref, order);
|
||||||
|
}),
|
||||||
|
]));
|
||||||
|
}
|
||||||
return groups;
|
return groups;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1033,61 +1235,112 @@ class _StockOutListScreenState extends ConsumerState<StockOutListScreen> {
|
|||||||
final controllers = {
|
final controllers = {
|
||||||
for (final it in pending) it.id!: TextEditingController()
|
for (final it in pending) it.id!: TextEditingController()
|
||||||
};
|
};
|
||||||
final ok = await showDialog<bool>(
|
final bool? ok;
|
||||||
context: context,
|
if (context.isMobile) {
|
||||||
builder: (ctx) => AlertDialog(
|
// 窄屏:底部 sheet(对齐原型 openConfirm)。
|
||||||
title: const Text('确认售价'),
|
ok = await showMSheet<bool>(
|
||||||
content: SizedBox(
|
context,
|
||||||
width: context.dialogWidth(440),
|
title: '确认售价',
|
||||||
child: SingleChildScrollView(
|
builder: (ctx) => Column(
|
||||||
child: Column(
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'「先出后定价」:填写真实售价后,系统据此结算应收(售价 × 数量)并补一条应收流水——不影响成本 / 库存,不反审核。',
|
'「先卖后定价」:填写真实售价后,系统按售价 × 数量结算应收并补一条应收流水——不反审核、不影响成本 / 应付。',
|
||||||
style: TextStyle(fontSize: 12, color: context.tokens.muted),
|
style: TextStyle(
|
||||||
),
|
fontSize: AppDims.fsSm, height: 1.6, color: ctx.tokens.muted),
|
||||||
const SizedBox(height: 12),
|
|
||||||
...pending.map((it) {
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 6),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
'${it.productName ?? ''} ${it.productSpec ?? ''} ×${it.quantity.toStringAsFixed(0)}',
|
|
||||||
style: const TextStyle(fontSize: 13),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
SizedBox(
|
|
||||||
width: 130,
|
|
||||||
child: TextField(
|
|
||||||
controller: controllers[it.id],
|
|
||||||
keyboardType: const TextInputType.numberWithOptions(
|
|
||||||
decimal: true),
|
|
||||||
decoration: const InputDecoration(
|
|
||||||
prefixText: '¥',
|
|
||||||
hintText: '真实售价',
|
|
||||||
isDense: true),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(height: 14),
|
||||||
|
for (final it in pending)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 12),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'${it.productName ?? ''} ${it.productSpec ?? ''} · 数量 ${it.quantity.toStringAsFixed(0)}',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: AppDims.fsSm, color: ctx.tokens.muted)),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
TextField(
|
||||||
|
controller: controllers[it.id],
|
||||||
|
keyboardType:
|
||||||
|
const TextInputType.numberWithOptions(decimal: true),
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
prefixText: '¥',
|
||||||
|
hintText: '真实售价 0.00',
|
||||||
|
isDense: true),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
DsButton('取消', onPressed: () => Navigator.pop(ctx, false)),
|
DsButton('取消', onPressed: () => Navigator.of(context).pop(false)),
|
||||||
DsButton('确认并补应收',
|
DsButton('确认并补应收',
|
||||||
variant: DsBtnVariant.primary,
|
variant: DsBtnVariant.primary,
|
||||||
onPressed: () => Navigator.pop(ctx, true)),
|
onPressed: () => Navigator.of(context).pop(true)),
|
||||||
],
|
],
|
||||||
),
|
);
|
||||||
);
|
} else {
|
||||||
|
ok = await showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (ctx) => AlertDialog(
|
||||||
|
title: const Text('确认售价'),
|
||||||
|
content: SizedBox(
|
||||||
|
width: context.dialogWidth(440),
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'「先出后定价」:填写真实售价后,系统据此结算应收(售价 × 数量)并补一条应收流水——不影响成本 / 库存,不反审核。',
|
||||||
|
style: TextStyle(fontSize: 12, color: context.tokens.muted),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
...pending.map((it) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 6),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
'${it.productName ?? ''} ${it.productSpec ?? ''} ×${it.quantity.toStringAsFixed(0)}',
|
||||||
|
style: const TextStyle(fontSize: 13),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
SizedBox(
|
||||||
|
width: 130,
|
||||||
|
child: TextField(
|
||||||
|
controller: controllers[it.id],
|
||||||
|
keyboardType:
|
||||||
|
const TextInputType.numberWithOptions(
|
||||||
|
decimal: true),
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
prefixText: '¥',
|
||||||
|
hintText: '真实售价',
|
||||||
|
isDense: true),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
DsButton('取消', onPressed: () => Navigator.pop(ctx, false)),
|
||||||
|
DsButton('确认并补应收',
|
||||||
|
variant: DsBtnVariant.primary,
|
||||||
|
onPressed: () => Navigator.pop(ctx, true)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
final items = <Map<String, dynamic>>[];
|
final items = <Map<String, dynamic>>[];
|
||||||
if (ok == true) {
|
if (ok == true) {
|
||||||
for (final it in pending) {
|
for (final it in pending) {
|
||||||
@@ -1795,3 +2048,274 @@ class _MenuChip extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── 详细搜索(窄屏底部 sheet,竖排单列;字段与桌面 _AdvancedSearchDialog 完全
|
||||||
|
// 同集,对齐原型 drawAdv;重置/应用按钮由 showMSheet actions 注入)────────
|
||||||
|
class _AdvSearchSheet extends ConsumerStatefulWidget {
|
||||||
|
final _AdvResult initial;
|
||||||
|
const _AdvSearchSheet({super.key, required this.initial});
|
||||||
|
|
||||||
|
@override
|
||||||
|
ConsumerState<_AdvSearchSheet> createState() => _AdvSearchSheetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AdvSearchSheetState extends ConsumerState<_AdvSearchSheet> {
|
||||||
|
late final TextEditingController _orderNoCtrl;
|
||||||
|
late final TextEditingController _productCtrl;
|
||||||
|
late final TextEditingController _productCodeCtrl;
|
||||||
|
late final TextEditingController _batchCtrl;
|
||||||
|
late int? _partnerId;
|
||||||
|
late String _series;
|
||||||
|
late String _spec;
|
||||||
|
late int? _operatorId;
|
||||||
|
late int? _reviewerId;
|
||||||
|
late String _status;
|
||||||
|
DateTimeRange? _dateRange;
|
||||||
|
|
||||||
|
static const _statusCodes = ['', 'draft', 'pending', 'approved', 'rejected'];
|
||||||
|
static const _statusNames = ['全部', '草稿', '待审核', '已审核', '已拒绝'];
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
final i = widget.initial;
|
||||||
|
_orderNoCtrl = TextEditingController(text: i.orderNo);
|
||||||
|
_productCtrl = TextEditingController(text: i.productInfo);
|
||||||
|
_productCodeCtrl = TextEditingController(text: i.productCode);
|
||||||
|
_batchCtrl = TextEditingController(text: i.batch);
|
||||||
|
_partnerId = i.partnerId;
|
||||||
|
_series = i.series;
|
||||||
|
_spec = i.spec;
|
||||||
|
_operatorId = i.operatorId;
|
||||||
|
_reviewerId = i.reviewerId;
|
||||||
|
_status = i.status;
|
||||||
|
_dateRange = i.dateRange;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_orderNoCtrl.dispose();
|
||||||
|
_productCtrl.dispose();
|
||||||
|
_productCodeCtrl.dispose();
|
||||||
|
_batchCtrl.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 重置(sheet actions「重置」调用)。
|
||||||
|
void reset() {
|
||||||
|
setState(() {
|
||||||
|
_orderNoCtrl.clear();
|
||||||
|
_productCtrl.clear();
|
||||||
|
_productCodeCtrl.clear();
|
||||||
|
_batchCtrl.clear();
|
||||||
|
_partnerId = null;
|
||||||
|
_series = '';
|
||||||
|
_spec = '';
|
||||||
|
_operatorId = null;
|
||||||
|
_reviewerId = null;
|
||||||
|
_status = '';
|
||||||
|
_dateRange = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 应用(sheet actions「应用」调用):组装结果并关闭 sheet。
|
||||||
|
void apply() {
|
||||||
|
Navigator.of(context).pop(_AdvResult(
|
||||||
|
orderNo: _orderNoCtrl.text.trim(),
|
||||||
|
productInfo: _productCtrl.text.trim(),
|
||||||
|
productCode: _productCodeCtrl.text.trim(),
|
||||||
|
batch: _batchCtrl.text.trim(),
|
||||||
|
partnerId: _partnerId,
|
||||||
|
series: _series,
|
||||||
|
spec: _spec,
|
||||||
|
operatorId: _operatorId,
|
||||||
|
reviewerId: _reviewerId,
|
||||||
|
status: _status,
|
||||||
|
dateRange: _dateRange,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _pickDate() async {
|
||||||
|
final range = await showWheelDateRange(context, initial: _dateRange);
|
||||||
|
if (range != null) setState(() => _dateRange = range);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final t = context.tokens;
|
||||||
|
final customers =
|
||||||
|
(ref.watch(allPartnersProvider).valueOrNull?.data ?? const [])
|
||||||
|
.map((p) => OptionItem(id: p.id, name: p.name, code: p.code))
|
||||||
|
.toList();
|
||||||
|
final seriesOpts =
|
||||||
|
(ref.watch(productSeriesListProvider).valueOrNull ?? const [])
|
||||||
|
.map((s) => OptionItem(id: s.id, name: s.name))
|
||||||
|
.toList();
|
||||||
|
final specOpts =
|
||||||
|
(ref.watch(productSpecListProvider).valueOrNull ?? const [])
|
||||||
|
.map((s) => OptionItem(id: s.id, name: s.name))
|
||||||
|
.toList();
|
||||||
|
final users = (ref.watch(userListProvider).valueOrNull ?? const [])
|
||||||
|
.map((u) => OptionItem(id: u.id, name: u.realName ?? u.username))
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
int? idByName(List<OptionItem> opts, String name) =>
|
||||||
|
opts.where((o) => o.name == name).firstOrNull?.id;
|
||||||
|
|
||||||
|
// 原型 .m-field:label + 控件竖排,单列。
|
||||||
|
Widget field(String label, Widget child) => Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 12),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text(label,
|
||||||
|
style: TextStyle(fontSize: AppDims.fsSm, color: t.muted)),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
child,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// 原型 .m-input:h42 带框文本输入。
|
||||||
|
Widget textField(TextEditingController c, String hint) => Container(
|
||||||
|
height: 42,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 11),
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: t.surface,
|
||||||
|
border: Border.all(color: t.border),
|
||||||
|
borderRadius: BorderRadius.circular(AppDims.rMd),
|
||||||
|
),
|
||||||
|
child: TextField(
|
||||||
|
controller: c,
|
||||||
|
style: TextStyle(fontSize: AppDims.fsBody, color: t.text),
|
||||||
|
decoration: InputDecoration(
|
||||||
|
isCollapsed: true,
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
|
border: InputBorder.none,
|
||||||
|
filled: false,
|
||||||
|
enabledBorder: InputBorder.none,
|
||||||
|
focusedBorder: InputBorder.none,
|
||||||
|
hintText: hint,
|
||||||
|
hintStyle: TextStyle(color: t.faint, fontSize: AppDims.fsBody),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget combo({
|
||||||
|
required List<OptionItem> opts,
|
||||||
|
required int? selectedId,
|
||||||
|
required String hint,
|
||||||
|
required ValueChanged<int?> onChanged,
|
||||||
|
}) =>
|
||||||
|
ComboSearchField(
|
||||||
|
options: opts,
|
||||||
|
selectedId: selectedId,
|
||||||
|
hint: hint,
|
||||||
|
onChanged: onChanged,
|
||||||
|
height: 42,
|
||||||
|
fillColor: t.surface,
|
||||||
|
);
|
||||||
|
|
||||||
|
final statusItems = [
|
||||||
|
for (var i = 0; i < _statusNames.length; i++)
|
||||||
|
OptionItem(id: i, name: _statusNames[i]),
|
||||||
|
];
|
||||||
|
final statusSelIdx = _statusCodes.indexOf(_status);
|
||||||
|
|
||||||
|
final dateHas = _dateRange != null;
|
||||||
|
final dateField = InkWell(
|
||||||
|
onTap: _pickDate,
|
||||||
|
borderRadius: BorderRadius.circular(AppDims.rMd),
|
||||||
|
child: Container(
|
||||||
|
height: 42,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 11),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: t.surface,
|
||||||
|
border: Border.all(color: t.border),
|
||||||
|
borderRadius: BorderRadius.circular(AppDims.rMd),
|
||||||
|
),
|
||||||
|
child: Row(children: [
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
dateHas
|
||||||
|
? '${DateFormat('yyyy-MM-dd').format(_dateRange!.start)} ~ ${DateFormat('yyyy-MM-dd').format(_dateRange!.end)}'
|
||||||
|
: '全部时间',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: AppDims.fsBody, color: dateHas ? t.text : t.faint),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (dateHas)
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () => setState(() => _dateRange = null),
|
||||||
|
child: Icon(LucideIcons.x, size: 14, color: t.muted),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
Icon(LucideIcons.calendar, size: 16, color: t.faint),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
field('单据编号', textField(_orderNoCtrl, '如 CK20260401001')),
|
||||||
|
field(
|
||||||
|
'客户',
|
||||||
|
combo(
|
||||||
|
opts: customers,
|
||||||
|
selectedId: _partnerId,
|
||||||
|
hint: '全部客户',
|
||||||
|
onChanged: (v) => setState(() => _partnerId = v))),
|
||||||
|
field('商品名', textField(_productCtrl, '输入酒名')),
|
||||||
|
field('商品编码', textField(_productCodeCtrl, '输入商品编码')),
|
||||||
|
field(
|
||||||
|
'系列',
|
||||||
|
combo(
|
||||||
|
opts: seriesOpts,
|
||||||
|
selectedId: idByName(seriesOpts, _series),
|
||||||
|
hint: '全部系列',
|
||||||
|
onChanged: (id) => setState(() => _series = id == null
|
||||||
|
? ''
|
||||||
|
: seriesOpts.firstWhere((o) => o.id == id).name))),
|
||||||
|
field(
|
||||||
|
'规格',
|
||||||
|
combo(
|
||||||
|
opts: specOpts,
|
||||||
|
selectedId: idByName(specOpts, _spec),
|
||||||
|
hint: '全部规格',
|
||||||
|
onChanged: (id) => setState(() => _spec = id == null
|
||||||
|
? ''
|
||||||
|
: specOpts.firstWhere((o) => o.id == id).name))),
|
||||||
|
field('批次号', textField(_batchCtrl, '输入批次号')),
|
||||||
|
field(
|
||||||
|
'状态',
|
||||||
|
ComboSearchField(
|
||||||
|
options: statusItems,
|
||||||
|
selectedId: statusSelIdx <= 0 ? null : statusSelIdx,
|
||||||
|
hint: '全部',
|
||||||
|
height: 42,
|
||||||
|
fillColor: t.surface,
|
||||||
|
onChanged: (id) => setState(
|
||||||
|
() => _status = (id == null) ? '' : _statusCodes[id]),
|
||||||
|
)),
|
||||||
|
field(
|
||||||
|
'出库员',
|
||||||
|
combo(
|
||||||
|
opts: users,
|
||||||
|
selectedId: _operatorId,
|
||||||
|
hint: '全部出库员',
|
||||||
|
onChanged: (id) => setState(() => _operatorId = id))),
|
||||||
|
field(
|
||||||
|
'审核人',
|
||||||
|
combo(
|
||||||
|
opts: users,
|
||||||
|
selectedId: _reviewerId,
|
||||||
|
hint: '全部审核人',
|
||||||
|
onChanged: (id) => setState(() => _reviewerId = id))),
|
||||||
|
field('单据日期', dateField),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
|
After Width: | Height: | Size: 149 KiB |
|
After Width: | Height: | Size: 148 KiB |
|
After Width: | Height: | Size: 150 KiB |
|
After Width: | Height: | Size: 139 KiB |
|
After Width: | Height: | Size: 138 KiB |
|
After Width: | Height: | Size: 140 KiB |
|
Before Width: | Height: | Size: 177 KiB After Width: | Height: | Size: 194 KiB |
|
Before Width: | Height: | Size: 178 KiB After Width: | Height: | Size: 195 KiB |
|
Before Width: | Height: | Size: 180 KiB After Width: | Height: | Size: 196 KiB |
|
Before Width: | Height: | Size: 170 KiB After Width: | Height: | Size: 157 KiB |
|
Before Width: | Height: | Size: 170 KiB After Width: | Height: | Size: 158 KiB |
|
Before Width: | Height: | Size: 172 KiB After Width: | Height: | Size: 159 KiB |
@@ -0,0 +1,115 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
import 'package:jiu_client/core/models/page_result.dart';
|
||||||
|
import 'package:jiu_client/core/auth/auth_state.dart';
|
||||||
|
import 'package:jiu_client/models/stock_in.dart';
|
||||||
|
import 'package:jiu_client/models/stock_summary.dart';
|
||||||
|
import 'package:jiu_client/providers/stock_in_provider.dart';
|
||||||
|
import 'package:jiu_client/screens/stock_in/stock_in_list_screen.dart';
|
||||||
|
|
||||||
|
import '../support/golden_harness.dart';
|
||||||
|
|
||||||
|
/// P1 单据域:入库列表窄屏 golden × 三主题(390×844 @2x)——对齐移动原型
|
||||||
|
/// m-stock-in-list.html:KPI 2×2(待审核/草稿可点筛选)+ MSearchRow(搜索 +
|
||||||
|
/// 状态钮 + 详搜钮)+ .m-section + 卡片流(图标徽章含派生态 + › 箭头)。
|
||||||
|
/// 更新基准:flutter test --update-goldens test/golden/stock_in_list_mobile_golden_test.dart
|
||||||
|
|
||||||
|
const _orders = [
|
||||||
|
// 已审核 + 部分退单 + 待定价(三徽章派生态齐全)
|
||||||
|
StockInOrder(
|
||||||
|
id: 1,
|
||||||
|
orderNo: 'RK-20260620-014',
|
||||||
|
warehouseId: 1,
|
||||||
|
warehouseName: '主仓',
|
||||||
|
partnerName: '贵州茅台经销',
|
||||||
|
operatorName: '王经理',
|
||||||
|
reviewerName: '张管理',
|
||||||
|
status: 'approved',
|
||||||
|
returnState: 'partial',
|
||||||
|
orderDate: '2026-06-20',
|
||||||
|
costTotal: 34320,
|
||||||
|
items: [
|
||||||
|
StockInItem(productId: 1, quantity: 6, costPrice: 0, costAmount: 0),
|
||||||
|
StockInItem(
|
||||||
|
productId: 2, quantity: 6, costPrice: 2680, costAmount: 16080),
|
||||||
|
]),
|
||||||
|
StockInOrder(
|
||||||
|
id: 2,
|
||||||
|
orderNo: 'RK-20260618-009',
|
||||||
|
warehouseId: 1,
|
||||||
|
warehouseName: '主仓',
|
||||||
|
partnerName: '宜宾五粮液',
|
||||||
|
operatorName: '李销售',
|
||||||
|
status: 'pending',
|
||||||
|
orderDate: '2026-06-18',
|
||||||
|
costTotal: 6700),
|
||||||
|
StockInOrder(
|
||||||
|
id: 3,
|
||||||
|
orderNo: 'RK-20260615-003',
|
||||||
|
warehouseId: 1,
|
||||||
|
warehouseName: '二号仓',
|
||||||
|
partnerName: '泸州老窖',
|
||||||
|
operatorName: '王经理',
|
||||||
|
status: 'draft',
|
||||||
|
orderDate: '2026-06-15',
|
||||||
|
costTotal: 5000),
|
||||||
|
StockInOrder(
|
||||||
|
id: 4,
|
||||||
|
orderNo: 'RK-20260612-001',
|
||||||
|
warehouseId: 1,
|
||||||
|
warehouseName: '主仓',
|
||||||
|
partnerName: '绵竹剑南春',
|
||||||
|
operatorName: '李销售',
|
||||||
|
reviewerName: '张管理',
|
||||||
|
status: 'rejected',
|
||||||
|
orderDate: '2026-06-12',
|
||||||
|
costTotal: 3500),
|
||||||
|
];
|
||||||
|
|
||||||
|
class _FakeStockInNotifier extends StockInListNotifier {
|
||||||
|
@override
|
||||||
|
Future<PageResult<StockInOrder>> build() async =>
|
||||||
|
const PageResult(data: _orders, total: 4, page: 1, pageSize: 20);
|
||||||
|
@override
|
||||||
|
void setPage(int page) {}
|
||||||
|
@override
|
||||||
|
void setPageSize(int pageSize) {}
|
||||||
|
@override
|
||||||
|
void setStatus(String status) {}
|
||||||
|
@override
|
||||||
|
void setDateRange(String? startDate, String? endDate) {}
|
||||||
|
@override
|
||||||
|
void setKeyword(String keyword) {}
|
||||||
|
@override
|
||||||
|
void reload() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Override> _overrides() => [
|
||||||
|
stockInListProvider.overrideWith(() => _FakeStockInNotifier()),
|
||||||
|
isReadonlyProvider.overrideWithValue(false),
|
||||||
|
// KPI 2×2(近30天笔数/金额/待审核/草稿):对齐原型示例数值。
|
||||||
|
stockInSummary30Provider.overrideWith((ref) => const StockSummary(
|
||||||
|
monthCount: 28,
|
||||||
|
monthAmount: 1860000,
|
||||||
|
pendingCount: 4,
|
||||||
|
draftCount: 2,
|
||||||
|
lastMonthCount: 25,
|
||||||
|
lastMonthAmount: 1715000,
|
||||||
|
)),
|
||||||
|
];
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
TestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
SharedPreferences.setMockInitialValues({});
|
||||||
|
|
||||||
|
goldenAcrossThemes(
|
||||||
|
'stock-in list 窄屏(移动原型)',
|
||||||
|
goldenPrefix: 'm_stock_in_list',
|
||||||
|
child: () => const Scaffold(body: StockInListScreen()),
|
||||||
|
overrides: _overrides,
|
||||||
|
logical: const Size(390, 844),
|
||||||
|
dpr: 2,
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
import 'package:jiu_client/core/models/page_result.dart';
|
||||||
|
import 'package:jiu_client/core/auth/auth_state.dart';
|
||||||
|
import 'package:jiu_client/models/stock_out.dart';
|
||||||
|
import 'package:jiu_client/models/stock_summary.dart';
|
||||||
|
import 'package:jiu_client/providers/stock_out_provider.dart';
|
||||||
|
import 'package:jiu_client/screens/stock_out/stock_out_list_screen.dart';
|
||||||
|
|
||||||
|
import '../support/golden_harness.dart';
|
||||||
|
|
||||||
|
/// P1 单据域:出库列表窄屏 golden × 三主题(390×844 @2x)——对齐移动原型
|
||||||
|
/// m-stock-out-list.html:KPI 2×2(待审核/草稿可点筛选)+ MSearchRow(搜索 +
|
||||||
|
/// 状态钮 + 详搜钮)+ .m-section + 卡片流(图标徽章含派生态 + › 箭头)。
|
||||||
|
/// 更新基准:flutter test --update-goldens test/golden/stock_out_list_mobile_golden_test.dart
|
||||||
|
|
||||||
|
const _orders = [
|
||||||
|
// 已审核 + 部分退单(图标徽章派生态)
|
||||||
|
StockOutOrder(
|
||||||
|
id: 1,
|
||||||
|
orderNo: 'CK-20260620-031',
|
||||||
|
warehouseId: 1,
|
||||||
|
warehouseName: '主仓',
|
||||||
|
partnerName: '鼎丰超市',
|
||||||
|
operatorName: '李销售',
|
||||||
|
reviewerName: '张管理',
|
||||||
|
status: 'approved',
|
||||||
|
returnState: 'partial',
|
||||||
|
orderDate: '2026-06-20',
|
||||||
|
saleTotal: 12800,
|
||||||
|
items: [
|
||||||
|
StockOutItem(
|
||||||
|
productId: 1,
|
||||||
|
quantity: 4,
|
||||||
|
salePrice: 3200,
|
||||||
|
saleAmount: 12800,
|
||||||
|
costPrice: 2680,
|
||||||
|
costAmount: 10720),
|
||||||
|
]),
|
||||||
|
// 已审核 + 待定价(先卖后定价)
|
||||||
|
StockOutOrder(
|
||||||
|
id: 2,
|
||||||
|
orderNo: 'CK-20260619-026',
|
||||||
|
warehouseId: 1,
|
||||||
|
warehouseName: '名酒仓',
|
||||||
|
partnerName: '金樽会所',
|
||||||
|
operatorName: '王经理',
|
||||||
|
reviewerName: '张管理',
|
||||||
|
status: 'approved',
|
||||||
|
orderDate: '2026-06-19',
|
||||||
|
saleTotal: 28000,
|
||||||
|
items: [
|
||||||
|
StockOutItem(productId: 2, quantity: 13, salePrice: 0, saleAmount: 0),
|
||||||
|
]),
|
||||||
|
StockOutOrder(
|
||||||
|
id: 3,
|
||||||
|
orderNo: 'CK-20260618-022',
|
||||||
|
warehouseId: 1,
|
||||||
|
warehouseName: '主仓',
|
||||||
|
partnerName: '金樽酒楼',
|
||||||
|
operatorName: '张前台',
|
||||||
|
status: 'pending',
|
||||||
|
orderDate: '2026-06-18',
|
||||||
|
saleTotal: 5400),
|
||||||
|
StockOutOrder(
|
||||||
|
id: 4,
|
||||||
|
orderNo: 'CK-20260615-017',
|
||||||
|
warehouseId: 1,
|
||||||
|
warehouseName: '二号仓',
|
||||||
|
partnerName: '恒昌名酒行',
|
||||||
|
operatorName: '李销售',
|
||||||
|
status: 'draft',
|
||||||
|
orderDate: '2026-06-15',
|
||||||
|
saleTotal: 3200),
|
||||||
|
];
|
||||||
|
|
||||||
|
class _FakeStockOutNotifier extends StockOutListNotifier {
|
||||||
|
@override
|
||||||
|
Future<PageResult<StockOutOrder>> build() async =>
|
||||||
|
const PageResult(data: _orders, total: 4, page: 1, pageSize: 20);
|
||||||
|
@override
|
||||||
|
void setPage(int page) {}
|
||||||
|
@override
|
||||||
|
void setPageSize(int pageSize) {}
|
||||||
|
@override
|
||||||
|
void setStatus(String status) {}
|
||||||
|
@override
|
||||||
|
void setDateRange(String? startDate, String? endDate) {}
|
||||||
|
@override
|
||||||
|
void setKeyword(String keyword) {}
|
||||||
|
@override
|
||||||
|
void setProductCode(String code) {}
|
||||||
|
@override
|
||||||
|
void reload() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Override> _overrides() => [
|
||||||
|
stockOutListProvider.overrideWith(() => _FakeStockOutNotifier()),
|
||||||
|
isReadonlyProvider.overrideWithValue(false),
|
||||||
|
// KPI 2×2(近30天笔数/金额/待审核/草稿):对齐原型示例数值。
|
||||||
|
stockOutSummary30Provider.overrideWith((ref) => const StockSummary(
|
||||||
|
monthCount: 35,
|
||||||
|
monthAmount: 2160000,
|
||||||
|
pendingCount: 3,
|
||||||
|
draftCount: 1,
|
||||||
|
lastMonthCount: 32,
|
||||||
|
lastMonthAmount: 2022000,
|
||||||
|
)),
|
||||||
|
];
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
TestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
SharedPreferences.setMockInitialValues({});
|
||||||
|
|
||||||
|
goldenAcrossThemes(
|
||||||
|
'stock-out list 窄屏(移动原型)',
|
||||||
|
goldenPrefix: 'm_stock_out_list',
|
||||||
|
child: () => const Scaffold(body: StockOutListScreen()),
|
||||||
|
overrides: _overrides,
|
||||||
|
logical: const Size(390, 844),
|
||||||
|
dpr: 2,
|
||||||
|
);
|
||||||
|
}
|
||||||