feat(client): 出入库列表状态筛选改多选并默认草稿+待审核(两端,原型同步)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@@ -38,7 +38,8 @@ final stockInSummary30Provider =
|
||||
class StockInListNotifier extends AsyncNotifier<PageResult<StockInOrder>> {
|
||||
int _page = 1;
|
||||
int _pageSize = AppConstants.defaultPageSize;
|
||||
String _status = '';
|
||||
// 默认「草稿+待审核」(2026-07-14 用户拍板,状态筛选改多选后的默认态)。
|
||||
String _status = 'draft,pending';
|
||||
String? _startDate;
|
||||
String? _endDate;
|
||||
String _keyword = '';
|
||||
|
||||
@@ -38,7 +38,8 @@ final stockOutSummary30Provider =
|
||||
class StockOutListNotifier extends AsyncNotifier<PageResult<StockOutOrder>> {
|
||||
int _page = 1;
|
||||
int _pageSize = AppConstants.defaultPageSize;
|
||||
String _status = '';
|
||||
// 默认「草稿+待审核」(2026-07-14 用户拍板,状态筛选改多选后的默认态)。
|
||||
String _status = 'draft,pending';
|
||||
String? _startDate;
|
||||
String? _endDate;
|
||||
String _keyword = '';
|
||||
|
||||
@@ -17,6 +17,7 @@ import '../../models/stock_summary.dart';
|
||||
import '../../providers/stock_in_provider.dart';
|
||||
import '../../widgets/ds/ds_atoms.dart';
|
||||
import '../../widgets/ds/ds_kpi.dart';
|
||||
import '../../widgets/ds/ds_menu.dart';
|
||||
import '../../widgets/ds/ds_table.dart';
|
||||
import '../../widgets/ds/m_kpi_grid.dart';
|
||||
import '../../widgets/ds/m_search_row.dart';
|
||||
@@ -53,11 +54,13 @@ class StockInListScreen extends ConsumerStatefulWidget {
|
||||
|
||||
class _StockInListScreenState extends ConsumerState<StockInListScreen> {
|
||||
final _searchCtrl = TextEditingController();
|
||||
String _statusFilter = ''; // '' = 全部
|
||||
// 状态筛选 2026-07-14 改多选:主状态(草稿/待审核/已审核/已拒绝)多选,空集=全部;
|
||||
// 默认「草稿+待审核」(D3 用户拍板)。退单伪状态走 _detail['return_state'],与主状态互斥(D4)。
|
||||
Set<String> _statusFilter = {'draft', 'pending'};
|
||||
DateTimeRange? _dateRange;
|
||||
String _datePresetLabel = ''; // 入库时间 chip 显示的预设名(近7天/本月/自定义…)
|
||||
String? _warehouseName; // 仓库客户端筛选(单选)
|
||||
// 详细搜索多字段(含 partner_id,与工具栏供应商联动)。key 用后端参数名。
|
||||
// 详细搜索多字段(含 partner_id,与工具栏供应商联动;return_state 也走这里)。key 用后端参数名。
|
||||
Map<String, String> _detail = const {};
|
||||
|
||||
// 表格列(照原型顺序,固定列,无列设置)。
|
||||
@@ -73,25 +76,28 @@ class _StockInListScreenState extends ConsumerState<StockInListScreen> {
|
||||
('actions', '操作'),
|
||||
];
|
||||
|
||||
static const _statusOptions = [
|
||||
('', '全部'),
|
||||
('draft', '草稿'),
|
||||
('pending', '待审核'),
|
||||
('approved', '已审核'),
|
||||
('rejected', '已拒绝'),
|
||||
// 退单状态(return_state):与主状态互斥,走 detail 服务端过滤。
|
||||
('ret:partial', '部分退单'),
|
||||
('ret:full', '已退单'),
|
||||
];
|
||||
|
||||
// 单视图已知主状态;未知值(如旧版残留的 'pending,draft' 多值)进页面时归零。
|
||||
static const _knownStatuses = {
|
||||
'',
|
||||
'draft',
|
||||
'pending',
|
||||
'approved',
|
||||
'rejected'
|
||||
// 主状态(多选)编码 → 中文标签。
|
||||
static const _mainCodes = ['draft', 'pending', 'approved', 'rejected'];
|
||||
static const _mainLabels = {
|
||||
'draft': '草稿',
|
||||
'pending': '待审核',
|
||||
'approved': '已审核',
|
||||
'rejected': '已拒绝',
|
||||
};
|
||||
// 退单伪状态(与主状态互斥,单选)编码 → 中文标签。
|
||||
static const _retCodes = ['partial', 'full'];
|
||||
static const _retLabels = {'partial': '部分退单', 'full': '已退单'};
|
||||
static const _defaultStatuses = {'draft', 'pending'};
|
||||
|
||||
String? get _retState => _detail['return_state'];
|
||||
|
||||
/// 状态是否为默认态(草稿+待审核、无退单)——仅用于「重置」按钮高亮判断。
|
||||
bool get _isDefaultStatus {
|
||||
final rs = _retState;
|
||||
return (rs == null || rs.isEmpty) &&
|
||||
_statusFilter.length == 2 &&
|
||||
_statusFilter.containsAll(_defaultStatuses);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -101,15 +107,31 @@ class _StockInListScreenState extends ConsumerState<StockInListScreen> {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted) return;
|
||||
final n = ref.read(stockInListProvider.notifier);
|
||||
if (!_knownStatuses.contains(n.currentStatus)) {
|
||||
n.setStatus(''); // 归零无法在单视图呈现的残留状态
|
||||
// 逗号拆分逐值校验合法性,非法(或全部非法)才归默认;'' 本身是合法的「全部」态,
|
||||
// 不可等同于非法一律重置——否则默认值 'draft,pending' 刚设就被清掉。
|
||||
final raw = n.currentStatus;
|
||||
if (raw.isEmpty) {
|
||||
_statusFilter = {};
|
||||
} else {
|
||||
_statusFilter = n.currentStatus;
|
||||
final parts = raw
|
||||
.split(',')
|
||||
.map((s) => s.trim())
|
||||
.where((s) => s.isNotEmpty)
|
||||
.toSet();
|
||||
final valid = parts.where(_mainCodes.contains).toSet();
|
||||
if (valid.isEmpty) {
|
||||
_statusFilter = Set.of(_defaultStatuses);
|
||||
n.setStatus(_statusFilter.join(','));
|
||||
} else {
|
||||
_statusFilter = valid;
|
||||
}
|
||||
}
|
||||
_searchCtrl.text = n.currentKeyword;
|
||||
_detail = Map<String, String>.of(n.currentDetail);
|
||||
final rs = _detail['return_state'];
|
||||
if (rs != null && rs.isNotEmpty) _statusFilter = 'ret:$rs';
|
||||
if (rs != null && rs.isNotEmpty) {
|
||||
_statusFilter = {}; // 退单态与主状态互斥,主状态显示清空
|
||||
}
|
||||
setState(() {});
|
||||
});
|
||||
}
|
||||
@@ -132,12 +154,24 @@ class _StockInListScreenState extends ConsumerState<StockInListScreen> {
|
||||
? '${_dateRange!.end.year}-${_dateRange!.end.month.toString().padLeft(2, '0')}-${_dateRange!.end.day.toString().padLeft(2, '0')}'
|
||||
: null;
|
||||
|
||||
/// 状态摘要:无=全部、1 项=值本身、多项=「N 项」;退单态优先展示(互斥)。
|
||||
String get _statusLabelOf {
|
||||
return _statusOptions
|
||||
.where((o) => o.$1 == _statusFilter)
|
||||
.map((o) => o.$2)
|
||||
.firstOrNull ??
|
||||
'全部';
|
||||
final rs = _retState;
|
||||
if (rs != null && rs.isNotEmpty) return _retLabels[rs] ?? '全部';
|
||||
if (_statusFilter.isEmpty) return '全部';
|
||||
if (_statusFilter.length == 1) {
|
||||
return _mainLabels[_statusFilter.first] ?? '全部';
|
||||
}
|
||||
return '${_statusFilter.length} 项';
|
||||
}
|
||||
|
||||
/// chip 摘要值(null=不显示,即「全部」态)。
|
||||
String? get _statusChipValue {
|
||||
final rs = _retState;
|
||||
if (rs != null && rs.isNotEmpty) return _retLabels[rs];
|
||||
if (_statusFilter.isEmpty) return null;
|
||||
if (_statusFilter.length == 1) return _mainLabels[_statusFilter.first];
|
||||
return '${_statusFilter.length} 项';
|
||||
}
|
||||
|
||||
OrderStatus _apiStatusToEnum(String status) {
|
||||
@@ -153,24 +187,88 @@ class _StockInListScreenState extends ConsumerState<StockInListScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
/// 覆盖式设置单一状态(D5:KPI 卡点击覆盖为该单值,不是 toggle 集合成员);
|
||||
/// `s` 为空表示「全部」,`ret:` 前缀表示退单伪状态(与主状态互斥)。
|
||||
void _setStatus(String s) {
|
||||
setState(() => _statusFilter = s);
|
||||
final n = ref.read(stockInListProvider.notifier);
|
||||
final d = Map<String, String>.of(_detail);
|
||||
if (s.startsWith('ret:')) {
|
||||
// 退单状态:走 return_state 服务端过滤,主状态清空(二者互斥)。
|
||||
d['return_state'] = s.substring(4);
|
||||
_detail = d;
|
||||
setState(() {
|
||||
_statusFilter = {};
|
||||
_detail = d;
|
||||
});
|
||||
n.setStatus('');
|
||||
n.setDetail(d);
|
||||
} else {
|
||||
d.remove('return_state');
|
||||
_detail = d;
|
||||
n.setStatus(s);
|
||||
setState(() {
|
||||
_statusFilter = s.isEmpty ? {} : {s};
|
||||
_detail = d;
|
||||
});
|
||||
n.setStatus(_statusFilter.join(','));
|
||||
n.setDetail(d);
|
||||
}
|
||||
}
|
||||
|
||||
/// 状态默认态(草稿+待审核)——chip「×」/「重置」用(D3:回默认,不是回「全部」)。
|
||||
void _setStatusDefault() {
|
||||
final d = Map<String, String>.of(_detail)..remove('return_state');
|
||||
setState(() {
|
||||
_statusFilter = Set.of(_defaultStatuses);
|
||||
_detail = d;
|
||||
});
|
||||
final n = ref.read(stockInListProvider.notifier);
|
||||
n.setStatus(_statusFilter.join(','));
|
||||
n.setDetail(d);
|
||||
}
|
||||
|
||||
/// 状态多选下拉:主状态 checkbox 多选 + 退单伪状态互斥(选其一清另一侧)。
|
||||
void _openStatusMenu(BuildContext anchorContext) {
|
||||
showDsMultiMenu<String>(
|
||||
anchorContext,
|
||||
itemsBuilder: () => [
|
||||
for (final c in _mainCodes)
|
||||
DsMenuItem(
|
||||
value: c,
|
||||
label: _mainLabels[c]!,
|
||||
selected: _statusFilter.contains(c)),
|
||||
for (final r in _retCodes)
|
||||
DsMenuItem(
|
||||
value: 'ret:$r',
|
||||
label: _retLabels[r]!,
|
||||
selected: _retState == r),
|
||||
],
|
||||
onToggle: (v) {
|
||||
if (v.startsWith('ret:')) {
|
||||
final code = v.substring(4);
|
||||
final turningOn = _retState != code;
|
||||
final d = Map<String, String>.of(_detail);
|
||||
if (turningOn) {
|
||||
d['return_state'] = code;
|
||||
} else {
|
||||
d.remove('return_state');
|
||||
}
|
||||
setState(() {
|
||||
_statusFilter = {};
|
||||
_detail = d;
|
||||
});
|
||||
} else {
|
||||
final next = Set.of(_statusFilter);
|
||||
next.contains(v) ? next.remove(v) : next.add(v);
|
||||
final d = Map<String, String>.of(_detail)..remove('return_state');
|
||||
setState(() {
|
||||
_statusFilter = next;
|
||||
_detail = d;
|
||||
});
|
||||
}
|
||||
final n = ref.read(stockInListProvider.notifier);
|
||||
n.setStatus(_statusFilter.join(','));
|
||||
n.setDetail(_detail);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _setDetailPartner(int? id) {
|
||||
final d = Map<String, String>.of(_detail);
|
||||
if (id == null) {
|
||||
@@ -185,7 +283,7 @@ class _StockInListScreenState extends ConsumerState<StockInListScreen> {
|
||||
void _resetFilters() {
|
||||
setState(() {
|
||||
_searchCtrl.clear();
|
||||
_statusFilter = '';
|
||||
_statusFilter = Set.of(_defaultStatuses); // D3:回默认「草稿+待审核」,不是回「全部」
|
||||
_dateRange = null;
|
||||
_datePresetLabel = '';
|
||||
_warehouseName = null;
|
||||
@@ -193,7 +291,7 @@ class _StockInListScreenState extends ConsumerState<StockInListScreen> {
|
||||
});
|
||||
final n = ref.read(stockInListProvider.notifier);
|
||||
n.setKeyword('');
|
||||
n.setStatus('');
|
||||
n.setStatus(_statusFilter.join(','));
|
||||
n.setDateRange(null, null);
|
||||
n.setDetail(const {});
|
||||
}
|
||||
@@ -270,6 +368,10 @@ class _StockInListScreenState extends ConsumerState<StockInListScreen> {
|
||||
),
|
||||
);
|
||||
|
||||
/// 详细搜索的「状态」字段仍单选(未扩展为多选):多选态无法单值呈现时归「全部」。
|
||||
String get _statusForAdvDialog =>
|
||||
_statusFilter.length == 1 ? _statusFilter.first : '';
|
||||
|
||||
Future<void> _openAdvSearch() async {
|
||||
final _AdvResult? res;
|
||||
if (context.isMobile) {
|
||||
@@ -281,7 +383,7 @@ class _StockInListScreenState extends ConsumerState<StockInListScreen> {
|
||||
builder: (_) => _AdvSearchSheet(
|
||||
key: formKey,
|
||||
initialDetail: _detail,
|
||||
initialStatus: _statusFilter,
|
||||
initialStatus: _statusForAdvDialog,
|
||||
initialRange: _dateRange,
|
||||
),
|
||||
actions: [
|
||||
@@ -296,7 +398,7 @@ class _StockInListScreenState extends ConsumerState<StockInListScreen> {
|
||||
context: context,
|
||||
builder: (_) => _AdvSearchDialog(
|
||||
initialDetail: _detail,
|
||||
initialStatus: _statusFilter,
|
||||
initialStatus: _statusForAdvDialog,
|
||||
initialRange: _dateRange,
|
||||
),
|
||||
);
|
||||
@@ -305,19 +407,20 @@ class _StockInListScreenState extends ConsumerState<StockInListScreen> {
|
||||
if (r == null || !mounted) return;
|
||||
setState(() {
|
||||
_detail = r.detail;
|
||||
_statusFilter = r.status;
|
||||
_statusFilter = r.status.isEmpty ? {} : {r.status};
|
||||
_dateRange = r.dateRange;
|
||||
});
|
||||
final n = ref.read(stockInListProvider.notifier);
|
||||
n.setStatus(r.status);
|
||||
n.setStatus(_statusFilter.join(','));
|
||||
n.setDateRange(_startDate, _endDate);
|
||||
n.setDetail(r.detail);
|
||||
}
|
||||
|
||||
int get _advCount {
|
||||
var c = _detail.entries.where((e) => e.value.trim().isNotEmpty).length;
|
||||
// 主状态也算一项(退单状态已作为 _detail.return_state 计入,故排除 ret:)。
|
||||
if (_statusFilter.isNotEmpty && !_statusFilter.startsWith('ret:')) c++;
|
||||
// 主状态非默认态才算一项(默认「草稿+待审核」不计入,避免刚进屏就显示已筛选;
|
||||
// 退单状态已作为 _detail.return_state 计入,此时 _statusFilter 为空不会重复加)。
|
||||
if (_statusFilter.isNotEmpty && !_isDefaultStatus) c++;
|
||||
return c;
|
||||
}
|
||||
|
||||
@@ -620,16 +723,20 @@ class _StockInListScreenState extends ConsumerState<StockInListScreen> {
|
||||
delta: pendingCount > 0 ? '需尽快处理' : '点击筛选',
|
||||
deltaTone:
|
||||
pendingCount > 0 ? MKpiDeltaTone.warn : MKpiDeltaTone.normal,
|
||||
selected: _statusFilter == 'pending',
|
||||
onTap: () =>
|
||||
_setStatus(_statusFilter == 'pending' ? '' : 'pending')),
|
||||
// D5:KPI 卡点击覆盖为该单值(非 toggle 集合成员)。
|
||||
selected: _retState == null &&
|
||||
_statusFilter.length == 1 &&
|
||||
_statusFilter.contains('pending'),
|
||||
onTap: () => _setStatus('pending')),
|
||||
MKpiItem(
|
||||
label: '草稿 · 点击筛选',
|
||||
value: '$draftCount',
|
||||
icon: LucideIcons.fileText,
|
||||
delta: draftCount > 0 ? '待提交审核' : '点击筛选',
|
||||
selected: _statusFilter == 'draft',
|
||||
onTap: () => _setStatus(_statusFilter == 'draft' ? '' : 'draft')),
|
||||
selected: _retState == null &&
|
||||
_statusFilter.length == 1 &&
|
||||
_statusFilter.contains('draft'),
|
||||
onTap: () => _setStatus('draft')),
|
||||
]),
|
||||
);
|
||||
}
|
||||
@@ -652,7 +759,7 @@ class _StockInListScreenState extends ConsumerState<StockInListScreen> {
|
||||
onSubmitted: (v) =>
|
||||
ref.read(stockInListProvider.notifier).setKeyword(v),
|
||||
statusLabel: _statusLabelOf,
|
||||
statusActive: _statusFilter.isNotEmpty,
|
||||
statusActive: _statusFilter.isNotEmpty || (_retState?.isNotEmpty ?? false),
|
||||
onStatusTap: _openStatusSheet,
|
||||
filterActive: _mobileAdvActive,
|
||||
onFilterTap: _openAdvSearch,
|
||||
@@ -676,53 +783,39 @@ class _StockInListScreenState extends ConsumerState<StockInListScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
/// 状态筛选底部 sheet(原型 openStatusSheet 的 m-opt 列表:图标 + 状态词 + 勾)。
|
||||
/// 状态筛选底部 sheet(多选勾选 + 确定应用,D5/D4):主状态多选 + 退单伪状态互斥。
|
||||
Future<void> _openStatusSheet() async {
|
||||
final sel = await showMSheet<String>(
|
||||
final formKey = GlobalKey<_StatusMultiSheetState>();
|
||||
final rsInit = _retState;
|
||||
final res = await showMSheet<_StatusSheetResult>(
|
||||
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 != '全部') ...[
|
||||
DsIconBadge(_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),
|
||||
]),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
builder: (_) => _StatusMultiSheet(
|
||||
key: formKey,
|
||||
initialStatuses: _statusFilter,
|
||||
initialRetState: (rsInit != null && rsInit.isNotEmpty) ? rsInit : null,
|
||||
),
|
||||
actions: [
|
||||
DsButton('重置', onPressed: () => formKey.currentState?.reset()),
|
||||
DsButton('确定',
|
||||
variant: DsBtnVariant.primary,
|
||||
onPressed: () => formKey.currentState?.apply()),
|
||||
],
|
||||
);
|
||||
if (sel == null || !mounted) return;
|
||||
_setStatus(sel);
|
||||
if (res == null || !mounted) return;
|
||||
final d = Map<String, String>.of(_detail);
|
||||
if (res.retState != null) {
|
||||
d['return_state'] = res.retState!;
|
||||
} else {
|
||||
d.remove('return_state');
|
||||
}
|
||||
setState(() {
|
||||
_statusFilter = res.statuses;
|
||||
_detail = d;
|
||||
});
|
||||
final n = ref.read(stockInListProvider.notifier);
|
||||
n.setStatus(_statusFilter.join(','));
|
||||
n.setDetail(d);
|
||||
}
|
||||
|
||||
// ── 工具栏(原型 .toolbar)─────────────────────────────────────────
|
||||
@@ -772,23 +865,15 @@ class _StockInListScreenState extends ConsumerState<StockInListScreen> {
|
||||
onClear: () => setState(() => _warehouseName = null)),
|
||||
);
|
||||
|
||||
final statusChip = PopupMenuButton<String>(
|
||||
onSelected: _setStatus,
|
||||
offset: const Offset(0, 40),
|
||||
color: ctx.tokens.surface,
|
||||
elevation: 8,
|
||||
shape: RoundedRectangleBorder(
|
||||
side: BorderSide(color: ctx.tokens.border),
|
||||
borderRadius: BorderRadius.circular(AppDims.rMd),
|
||||
// 状态:多选 chip(草稿/待审核/已审核/已拒绝 checkbox 多选 + 部分退单/已退单 互斥单选),
|
||||
// 摘要「值/N 项」,× 单清回默认「草稿+待审核」(D3/D4,照 inventory_list_screen 多选 chip 手法)。
|
||||
final statusChip = Builder(
|
||||
builder: (chipCtx) => DsChip(
|
||||
label: '状态',
|
||||
value: _statusChipValue,
|
||||
onTap: () => _openStatusMenu(chipCtx),
|
||||
onClear: _setStatusDefault,
|
||||
),
|
||||
itemBuilder: (_) => _statusOptions
|
||||
.map((s) =>
|
||||
PopupMenuItem(value: s.$1, height: 38, child: Text(s.$2)))
|
||||
.toList(),
|
||||
child: DsChip(
|
||||
label: '状态',
|
||||
value: _statusFilter.isEmpty ? null : _statusLabelOf,
|
||||
onClear: () => _setStatus('')),
|
||||
);
|
||||
|
||||
final dateChip = _dateMenu(ctx);
|
||||
@@ -1478,6 +1563,102 @@ class _StockInListScreenState extends ConsumerState<StockInListScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
// ── 状态筛选底部 sheet(原型 openStatusSheet 多选版):主状态 checkbox 多选 +
|
||||
// 退单伪状态互斥单选,底部「重置/确定」应用(对齐 D3/D4/Step5)─────────────
|
||||
class _StatusSheetResult {
|
||||
final Set<String> statuses;
|
||||
final String? retState; // null=未选退单态
|
||||
const _StatusSheetResult(this.statuses, this.retState);
|
||||
}
|
||||
|
||||
class _StatusMultiSheet extends StatefulWidget {
|
||||
final Set<String> initialStatuses;
|
||||
final String? initialRetState;
|
||||
const _StatusMultiSheet({
|
||||
super.key,
|
||||
required this.initialStatuses,
|
||||
required this.initialRetState,
|
||||
});
|
||||
|
||||
@override
|
||||
State<_StatusMultiSheet> createState() => _StatusMultiSheetState();
|
||||
}
|
||||
|
||||
class _StatusMultiSheetState extends State<_StatusMultiSheet> {
|
||||
late Set<String> _sel;
|
||||
String? _ret;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_sel = Set.of(widget.initialStatuses);
|
||||
_ret = widget.initialRetState;
|
||||
}
|
||||
|
||||
/// 重置(sheet actions「重置」调用):回默认「草稿+待审核」(D3)。
|
||||
void reset() {
|
||||
setState(() {
|
||||
_sel = Set.of(_StockInListScreenState._defaultStatuses);
|
||||
_ret = null;
|
||||
});
|
||||
}
|
||||
|
||||
/// 确定(sheet actions「确定」调用):组装结果并关闭 sheet。
|
||||
void apply() {
|
||||
Navigator.of(context).pop(_StatusSheetResult(_sel, _ret));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final t = context.tokens;
|
||||
final mainCodes = _StockInListScreenState._mainCodes;
|
||||
final mainLabels = _StockInListScreenState._mainLabels;
|
||||
final retCodes = _StockInListScreenState._retCodes;
|
||||
final retLabels = _StockInListScreenState._retLabels;
|
||||
|
||||
Widget row(String code, String label, {required bool isRet}) {
|
||||
final sel = isRet ? _ret == code : _sel.contains(code);
|
||||
return InkWell(
|
||||
onTap: () => setState(() {
|
||||
if (isRet) {
|
||||
_ret = _ret == code ? null : code;
|
||||
_sel = {};
|
||||
} else {
|
||||
final next = Set.of(_sel);
|
||||
next.contains(code) ? next.remove(code) : next.add(code);
|
||||
_sel = next;
|
||||
_ret = null;
|
||||
}
|
||||
}),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 13, horizontal: 4),
|
||||
decoration:
|
||||
BoxDecoration(border: Border(bottom: BorderSide(color: t.borderSubtle))),
|
||||
child: Row(children: [
|
||||
DsIconBadge(label),
|
||||
const SizedBox(width: 10),
|
||||
Text(label,
|
||||
style: TextStyle(
|
||||
fontSize: AppDims.fsBody,
|
||||
fontWeight: sel ? FontWeight.w600 : FontWeight.w400,
|
||||
color: sel ? t.primary : t.text)),
|
||||
const Spacer(),
|
||||
if (sel) Icon(LucideIcons.check, size: 18, color: t.primary),
|
||||
]),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
for (final c in mainCodes) row(c, mainLabels[c]!, isRet: false),
|
||||
for (final r in retCodes) row(r, retLabels[r]!, isRet: true),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ── 详细搜索按钮(原型 .btn.soft,浅蓝突出;有条件时转 primary)────────────
|
||||
class _AdvSearchButton extends StatelessWidget {
|
||||
final int count;
|
||||
|
||||
@@ -31,6 +31,7 @@ import '../../widgets/label_preview_dialog.dart';
|
||||
import '../../widgets/order_detail_drawer.dart';
|
||||
import '../../widgets/ds/ds_atoms.dart';
|
||||
import '../../widgets/ds/ds_kpi.dart';
|
||||
import '../../widgets/ds/ds_menu.dart';
|
||||
import '../../widgets/ds/ds_table.dart';
|
||||
import '../../widgets/ds/m_kpi_grid.dart';
|
||||
import '../../widgets/ds/m_search_row.dart';
|
||||
@@ -59,8 +60,9 @@ class StockOutListScreen extends ConsumerStatefulWidget {
|
||||
class _StockOutListScreenState extends ConsumerState<StockOutListScreen> {
|
||||
final _searchCtrl = TextEditingController();
|
||||
|
||||
// 状态('' = 全部)+ 日期区间:走 setStatus / setDateRange。
|
||||
String _statusFilter = '';
|
||||
// 状态筛选 2026-07-14 改多选:主状态(草稿/待审核/已审核/已拒绝)多选,空集=全部;
|
||||
// 默认「草稿+待审核」(D3 用户拍板)。退单伪状态仍走 _returnState,与主状态互斥(D4)。
|
||||
Set<String> _statusFilter = {'draft', 'pending'};
|
||||
DateTimeRange? _dateRange;
|
||||
String _appliedDateKey = '';
|
||||
String _datePresetLabel = ''; // 出库时间 chip 显示的预设名
|
||||
@@ -80,23 +82,24 @@ class _StockOutListScreenState extends ConsumerState<StockOutListScreen> {
|
||||
int? _reviewerId;
|
||||
String _returnState = ''; // '' / partial / full(退单状态,走 detail 服务端过滤)
|
||||
|
||||
static const _knownStatuses = {
|
||||
'',
|
||||
'draft',
|
||||
'pending',
|
||||
'approved',
|
||||
'rejected'
|
||||
// 主状态(多选)编码 → 中文标签。
|
||||
static const _mainCodes = ['draft', 'pending', 'approved', 'rejected'];
|
||||
static const _mainLabels = {
|
||||
'draft': '草稿',
|
||||
'pending': '待审核',
|
||||
'approved': '已审核',
|
||||
'rejected': '已拒绝',
|
||||
};
|
||||
static const _statusOptions = <(String, String)>[
|
||||
('', '全部'),
|
||||
('draft', '草稿'),
|
||||
('pending', '待审核'),
|
||||
('approved', '已审核'),
|
||||
('rejected', '已拒绝'),
|
||||
// 退单状态(return_state):与主状态互斥。
|
||||
('ret:partial', '部分退单'),
|
||||
('ret:full', '已退单'),
|
||||
];
|
||||
// 退单伪状态(与主状态互斥,单选)编码 → 中文标签。
|
||||
static const _retCodes = ['partial', 'full'];
|
||||
static const _retLabels = {'partial': '部分退单', 'full': '已退单'};
|
||||
static const _defaultStatuses = {'draft', 'pending'};
|
||||
|
||||
/// 状态是否为默认态(草稿+待审核、无退单)——仅用于「重置」按钮高亮判断。
|
||||
bool get _isDefaultStatus =>
|
||||
_returnState.isEmpty &&
|
||||
_statusFilter.length == 2 &&
|
||||
_statusFilter.containsAll(_defaultStatuses);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -106,10 +109,24 @@ class _StockOutListScreenState extends ConsumerState<StockOutListScreen> {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted) return;
|
||||
final n = ref.read(stockOutListProvider.notifier);
|
||||
if (!_knownStatuses.contains(n.currentStatus)) {
|
||||
n.setStatus('');
|
||||
// 逗号拆分逐值校验合法性,非法(或全部非法)才归默认;'' 本身是合法的「全部」态,
|
||||
// 不可等同于非法一律重置——否则默认值 'draft,pending' 刚设就被清掉。
|
||||
final raw = n.currentStatus;
|
||||
if (raw.isEmpty) {
|
||||
_statusFilter = {};
|
||||
} else {
|
||||
_statusFilter = n.currentStatus;
|
||||
final parts = raw
|
||||
.split(',')
|
||||
.map((s) => s.trim())
|
||||
.where((s) => s.isNotEmpty)
|
||||
.toSet();
|
||||
final valid = parts.where(_mainCodes.contains).toSet();
|
||||
if (valid.isEmpty) {
|
||||
_statusFilter = Set.of(_defaultStatuses);
|
||||
n.setStatus(_statusFilter.join(','));
|
||||
} else {
|
||||
_statusFilter = valid;
|
||||
}
|
||||
}
|
||||
_searchCtrl.text = n.currentKeyword;
|
||||
final d = n.currentDetail;
|
||||
@@ -123,7 +140,7 @@ class _StockOutListScreenState extends ConsumerState<StockOutListScreen> {
|
||||
_operatorId = int.tryParse(d['operator_id'] ?? '');
|
||||
_reviewerId = int.tryParse(d['reviewer_id'] ?? '');
|
||||
_returnState = d['return_state'] ?? '';
|
||||
if (_returnState.isNotEmpty) _statusFilter = 'ret:$_returnState';
|
||||
if (_returnState.isNotEmpty) _statusFilter = {}; // 退单态与主状态互斥
|
||||
setState(() {});
|
||||
});
|
||||
}
|
||||
@@ -162,18 +179,19 @@ class _StockOutListScreenState extends ConsumerState<StockOutListScreen> {
|
||||
if (_spec.isNotEmpty) c++;
|
||||
if (_operatorId != null) c++;
|
||||
if (_reviewerId != null) c++;
|
||||
// 状态也是详细搜索里的一项(含退单状态)——之前漏算导致徽标少 1。
|
||||
if (_statusFilter.isNotEmpty) c++;
|
||||
// 状态非默认态才算一项(默认「草稿+待审核」不计入,避免刚进屏就显示已筛选;
|
||||
// 含退单状态:选中退单态时 _statusFilter 为空,走下面单独判断)。
|
||||
if (_statusFilter.isNotEmpty && !_isDefaultStatus) c++;
|
||||
if (_returnState.isNotEmpty) c++;
|
||||
return c;
|
||||
}
|
||||
|
||||
bool get _hasAnyFilter =>
|
||||
_searchCtrl.text.isNotEmpty ||
|
||||
_statusFilter.isNotEmpty ||
|
||||
!_isDefaultStatus ||
|
||||
_dateRange != null ||
|
||||
_filterWarehouseName != null ||
|
||||
_partnerId != null ||
|
||||
_returnState.isNotEmpty ||
|
||||
_advCount > 0;
|
||||
|
||||
String? get _startDate => _dateRange != null
|
||||
@@ -194,7 +212,8 @@ class _StockOutListScreenState extends ConsumerState<StockOutListScreen> {
|
||||
final n = ref.read(stockOutListProvider.notifier);
|
||||
final detail = _buildDetail();
|
||||
if (!mapEquals(n.currentDetail, detail)) n.setDetail(detail);
|
||||
if (n.currentStatus != _statusFilter) n.setStatus(_statusFilter);
|
||||
final statusStr = _statusFilter.join(',');
|
||||
if (n.currentStatus != statusStr) n.setStatus(statusStr);
|
||||
final key = '${_startDate ?? ''}~${_endDate ?? ''}';
|
||||
if (key != _appliedDateKey) {
|
||||
_appliedDateKey = key;
|
||||
@@ -205,7 +224,7 @@ class _StockOutListScreenState extends ConsumerState<StockOutListScreen> {
|
||||
void _resetFilters() {
|
||||
setState(() {
|
||||
_searchCtrl.clear();
|
||||
_statusFilter = '';
|
||||
_statusFilter = Set.of(_defaultStatuses); // D3:回默认「草稿+待审核」,不是回「全部」
|
||||
_dateRange = null;
|
||||
_appliedDateKey = '';
|
||||
_datePresetLabel = '';
|
||||
@@ -223,7 +242,7 @@ class _StockOutListScreenState extends ConsumerState<StockOutListScreen> {
|
||||
});
|
||||
final n = ref.read(stockOutListProvider.notifier);
|
||||
n.setKeyword('');
|
||||
n.setStatus('');
|
||||
n.setStatus(_statusFilter.join(','));
|
||||
n.setDateRange(null, null);
|
||||
n.setDetail(const {});
|
||||
}
|
||||
@@ -545,10 +564,8 @@ class _StockOutListScreenState extends ConsumerState<StockOutListScreen> {
|
||||
icon: LucideIcons.clock,
|
||||
tone: DsKpiTone.alert,
|
||||
delta: (summary?.pendingCount ?? 0) > 0 ? '需尽快处理' : '点击筛选',
|
||||
onTap: () {
|
||||
setState(() => _statusFilter = 'pending');
|
||||
ref.read(stockOutListProvider.notifier).setStatus('pending');
|
||||
},
|
||||
// D5:KPI 卡点击覆盖为该单值(非 toggle 集合成员)。
|
||||
onTap: () => _setStatus('pending'),
|
||||
),
|
||||
DsKpi(
|
||||
title: '草稿单数 · 点击筛选',
|
||||
@@ -556,10 +573,7 @@ class _StockOutListScreenState extends ConsumerState<StockOutListScreen> {
|
||||
icon: LucideIcons.filePen,
|
||||
tone: DsKpiTone.warn, // 暗黄(用户拍板)
|
||||
delta: (summary?.draftCount ?? 0) > 0 ? '待提交审核' : '点击筛选',
|
||||
onTap: () {
|
||||
setState(() => _statusFilter = 'draft');
|
||||
ref.read(stockOutListProvider.notifier).setStatus('draft');
|
||||
},
|
||||
onTap: () => _setStatus('draft'),
|
||||
),
|
||||
];
|
||||
|
||||
@@ -591,26 +605,76 @@ class _StockOutListScreenState extends ConsumerState<StockOutListScreen> {
|
||||
|
||||
// ── 窄屏形态(对齐移动原型 m-stock-out-list.html)─────────────────
|
||||
|
||||
/// 状态设置(主状态 / ret:退单态互斥),KPI 卡与状态 sheet 共用。
|
||||
/// 覆盖式设置单一状态(D5:KPI 卡点击覆盖为该单值,不是 toggle 集合成员);
|
||||
/// `code` 为空表示「全部」,`ret:` 前缀表示退单伪状态(与主状态互斥)。
|
||||
void _setStatus(String code) {
|
||||
final n = ref.read(stockOutListProvider.notifier);
|
||||
if (code.startsWith('ret:')) {
|
||||
setState(() {
|
||||
_statusFilter = code;
|
||||
_statusFilter = {};
|
||||
_returnState = code.substring(4);
|
||||
});
|
||||
n.setStatus('');
|
||||
_applyDetail();
|
||||
} else {
|
||||
setState(() {
|
||||
_statusFilter = code;
|
||||
_statusFilter = code.isEmpty ? {} : {code};
|
||||
_returnState = '';
|
||||
});
|
||||
n.setStatus(code);
|
||||
n.setStatus(_statusFilter.join(','));
|
||||
_applyDetail();
|
||||
}
|
||||
}
|
||||
|
||||
/// 状态默认态(草稿+待审核)——chip「×」/「重置」用(D3:回默认,不是回「全部」)。
|
||||
void _setStatusDefault() {
|
||||
setState(() {
|
||||
_statusFilter = Set.of(_defaultStatuses);
|
||||
_returnState = '';
|
||||
});
|
||||
ref.read(stockOutListProvider.notifier).setStatus(_statusFilter.join(','));
|
||||
_applyDetail();
|
||||
}
|
||||
|
||||
/// 状态多选下拉:主状态 checkbox 多选 + 退单伪状态互斥(选其一清另一侧)。
|
||||
void _openStatusMenu(BuildContext anchorContext) {
|
||||
showDsMultiMenu<String>(
|
||||
anchorContext,
|
||||
itemsBuilder: () => [
|
||||
for (final c in _mainCodes)
|
||||
DsMenuItem(
|
||||
value: c,
|
||||
label: _mainLabels[c]!,
|
||||
selected: _statusFilter.contains(c)),
|
||||
for (final r in _retCodes)
|
||||
DsMenuItem(
|
||||
value: 'ret:$r',
|
||||
label: _retLabels[r]!,
|
||||
selected: _returnState == r),
|
||||
],
|
||||
onToggle: (v) {
|
||||
if (v.startsWith('ret:')) {
|
||||
final code = v.substring(4);
|
||||
setState(() {
|
||||
_returnState = _returnState == code ? '' : code;
|
||||
_statusFilter = {};
|
||||
});
|
||||
} else {
|
||||
setState(() {
|
||||
final next = Set.of(_statusFilter);
|
||||
next.contains(v) ? next.remove(v) : next.add(v);
|
||||
_statusFilter = next;
|
||||
_returnState = '';
|
||||
});
|
||||
}
|
||||
ref
|
||||
.read(stockOutListProvider.notifier)
|
||||
.setStatus(_statusFilter.join(','));
|
||||
_applyDetail();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// 状态词 → (前景, 软底):镜像原型 .badge.b-*,全走 token。
|
||||
/// 原型 .badge.ico:纯图标小徽章(卡片右上态标 / 状态 sheet 选项行)。
|
||||
/// 原型 .m-kpi 2×2:近30天笔数/金额 + 可点筛选的待审核/草稿(再点取消)。
|
||||
@@ -648,16 +712,20 @@ class _StockOutListScreenState extends ConsumerState<StockOutListScreen> {
|
||||
delta: pendingCount > 0 ? '需尽快处理' : '点击筛选',
|
||||
deltaTone:
|
||||
pendingCount > 0 ? MKpiDeltaTone.warn : MKpiDeltaTone.normal,
|
||||
selected: _statusFilter == 'pending',
|
||||
onTap: () =>
|
||||
_setStatus(_statusFilter == 'pending' ? '' : 'pending')),
|
||||
// D5:KPI 卡点击覆盖为该单值(非 toggle 集合成员)。
|
||||
selected: _returnState.isEmpty &&
|
||||
_statusFilter.length == 1 &&
|
||||
_statusFilter.contains('pending'),
|
||||
onTap: () => _setStatus('pending')),
|
||||
MKpiItem(
|
||||
label: '草稿 · 点击筛选',
|
||||
value: '$draftCount',
|
||||
icon: LucideIcons.fileText,
|
||||
delta: draftCount > 0 ? '待提交审核' : '点击筛选',
|
||||
selected: _statusFilter == 'draft',
|
||||
onTap: () => _setStatus(_statusFilter == 'draft' ? '' : 'draft')),
|
||||
selected: _returnState.isEmpty &&
|
||||
_statusFilter.length == 1 &&
|
||||
_statusFilter.contains('draft'),
|
||||
onTap: () => _setStatus('draft')),
|
||||
]),
|
||||
);
|
||||
}
|
||||
@@ -675,13 +743,26 @@ class _StockOutListScreenState extends ConsumerState<StockOutListScreen> {
|
||||
_reviewerId != null ||
|
||||
_dateRange != null;
|
||||
|
||||
/// 状态摘要:无=全部、1 项=值本身、多项=「N 项」;退单态优先展示(互斥)。
|
||||
String get _statusLabelOf {
|
||||
if (_returnState.isNotEmpty) return _retLabels[_returnState] ?? '全部';
|
||||
if (_statusFilter.isEmpty) return '全部';
|
||||
if (_statusFilter.length == 1) {
|
||||
return _mainLabels[_statusFilter.first] ?? '全部';
|
||||
}
|
||||
return '${_statusFilter.length} 项';
|
||||
}
|
||||
|
||||
/// chip 摘要值(null=不显示,即「全部」态)。
|
||||
String? get _statusChipValue {
|
||||
if (_returnState.isNotEmpty) return _retLabels[_returnState];
|
||||
if (_statusFilter.isEmpty) return null;
|
||||
if (_statusFilter.length == 1) return _mainLabels[_statusFilter.first];
|
||||
return '${_statusFilter.length} 项';
|
||||
}
|
||||
|
||||
/// 原型搜索区:搜索框 + 纯文字状态钮 + 图标详搜钮。
|
||||
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(
|
||||
@@ -691,8 +772,8 @@ class _StockOutListScreenState extends ConsumerState<StockOutListScreen> {
|
||||
onChanged: (v) => ref.read(stockOutListProvider.notifier).setKeyword(v),
|
||||
onSubmitted: (v) =>
|
||||
ref.read(stockOutListProvider.notifier).setKeyword(v),
|
||||
statusLabel: statusLabel,
|
||||
statusActive: _statusFilter.isNotEmpty,
|
||||
statusLabel: _statusLabelOf,
|
||||
statusActive: _statusFilter.isNotEmpty || _returnState.isNotEmpty,
|
||||
onStatusTap: _openStatusSheet,
|
||||
filterActive: _mobileAdvActive,
|
||||
onFilterTap: _openAdvSearch,
|
||||
@@ -716,53 +797,31 @@ class _StockOutListScreenState extends ConsumerState<StockOutListScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
/// 状态筛选底部 sheet(原型 openStatusSheet 的 m-opt 列表:图标 + 状态词 + 勾)。
|
||||
/// 状态筛选底部 sheet(多选勾选 + 确定应用,D5/D4):主状态多选 + 退单伪状态互斥。
|
||||
Future<void> _openStatusSheet() async {
|
||||
final sel = await showMSheet<String>(
|
||||
final formKey = GlobalKey<_StatusMultiSheetState>();
|
||||
final res = await showMSheet<_StatusSheetResult>(
|
||||
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 != '全部') ...[
|
||||
DsIconBadge(_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),
|
||||
]),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
builder: (_) => _StatusMultiSheet(
|
||||
key: formKey,
|
||||
initialStatuses: _statusFilter,
|
||||
initialRetState: _returnState.isEmpty ? null : _returnState,
|
||||
),
|
||||
actions: [
|
||||
DsButton('重置', onPressed: () => formKey.currentState?.reset()),
|
||||
DsButton('确定',
|
||||
variant: DsBtnVariant.primary,
|
||||
onPressed: () => formKey.currentState?.apply()),
|
||||
],
|
||||
);
|
||||
if (sel == null || !mounted) return;
|
||||
_setStatus(sel);
|
||||
if (res == null || !mounted) return;
|
||||
setState(() {
|
||||
_statusFilter = res.statuses;
|
||||
_returnState = res.retState ?? '';
|
||||
});
|
||||
ref.read(stockOutListProvider.notifier).setStatus(_statusFilter.join(','));
|
||||
_applyDetail();
|
||||
}
|
||||
|
||||
// ── 工具栏(原型 .toolbar)─────────────────────────────────────
|
||||
@@ -811,42 +870,15 @@ class _StockOutListScreenState extends ConsumerState<StockOutListScreen> {
|
||||
onClear: () => setState(() => _filterWarehouseName = null),
|
||||
);
|
||||
|
||||
// 状态:DsChip + 菜单。
|
||||
final statusLabel = _statusOptions
|
||||
.firstWhere((e) => e.$1 == _statusFilter, orElse: () => ('', ''))
|
||||
.$2;
|
||||
final statusChip = _MenuChip(
|
||||
label: '状态',
|
||||
value: _statusFilter.isEmpty ? null : statusLabel,
|
||||
options: _statusOptions.map((e) => e.$2).toList(),
|
||||
onSelected: (label) {
|
||||
final code = _statusOptions.firstWhere((e) => e.$2 == label).$1;
|
||||
final n = ref.read(stockOutListProvider.notifier);
|
||||
if (code.startsWith('ret:')) {
|
||||
// 退单状态:走 return_state,主状态清空(互斥)。
|
||||
setState(() {
|
||||
_statusFilter = code;
|
||||
_returnState = code.substring(4);
|
||||
});
|
||||
n.setStatus('');
|
||||
_applyDetail();
|
||||
} else {
|
||||
setState(() {
|
||||
_statusFilter = code;
|
||||
_returnState = '';
|
||||
});
|
||||
n.setStatus(code);
|
||||
_applyDetail();
|
||||
}
|
||||
},
|
||||
onClear: () {
|
||||
setState(() {
|
||||
_statusFilter = '';
|
||||
_returnState = '';
|
||||
});
|
||||
ref.read(stockOutListProvider.notifier).setStatus('');
|
||||
_applyDetail();
|
||||
},
|
||||
// 状态:多选 chip(草稿/待审核/已审核/已拒绝 checkbox 多选 + 部分退单/已退单 互斥单选),
|
||||
// 摘要「值/N 项」,× 单清回默认「草稿+待审核」(D3/D4,照 inventory_list_screen 多选 chip 手法)。
|
||||
final statusChip = Builder(
|
||||
builder: (chipCtx) => DsChip(
|
||||
label: '状态',
|
||||
value: _statusChipValue,
|
||||
onTap: () => _openStatusMenu(chipCtx),
|
||||
onClear: _setStatusDefault,
|
||||
),
|
||||
);
|
||||
|
||||
// anchorCtx = chip 自身位置:自定义范围下拉锚定在 chip 下方
|
||||
@@ -921,6 +953,10 @@ class _StockOutListScreenState extends ConsumerState<StockOutListScreen> {
|
||||
|
||||
// ── 详细搜索模态(11 字段)──────────────────────────────────────
|
||||
|
||||
/// 详细搜索的「状态」字段仍单选(未扩展为多选):多选态无法单值呈现时归「全部」。
|
||||
String get _statusForAdvDialog =>
|
||||
_statusFilter.length == 1 ? _statusFilter.first : '';
|
||||
|
||||
Future<void> _openAdvSearch() async {
|
||||
final initial = _AdvResult(
|
||||
orderNo: _orderNo,
|
||||
@@ -932,7 +968,7 @@ class _StockOutListScreenState extends ConsumerState<StockOutListScreen> {
|
||||
spec: _spec,
|
||||
operatorId: _operatorId,
|
||||
reviewerId: _reviewerId,
|
||||
status: _statusFilter,
|
||||
status: _statusForAdvDialog,
|
||||
dateRange: _dateRange,
|
||||
);
|
||||
final _AdvResult? result;
|
||||
@@ -968,7 +1004,7 @@ class _StockOutListScreenState extends ConsumerState<StockOutListScreen> {
|
||||
_spec = r.spec;
|
||||
_operatorId = r.operatorId;
|
||||
_reviewerId = r.reviewerId;
|
||||
_statusFilter = r.status;
|
||||
_statusFilter = r.status.isEmpty ? {} : {r.status};
|
||||
_dateRange = r.dateRange;
|
||||
});
|
||||
_applyAll();
|
||||
@@ -1573,6 +1609,102 @@ class _StockOutListScreenState extends ConsumerState<StockOutListScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
// ── 状态筛选底部 sheet(原型 openStatusSheet 多选版):主状态 checkbox 多选 +
|
||||
// 退单伪状态互斥单选,底部「重置/确定」应用(对齐 D3/D4/Step5)─────────────
|
||||
class _StatusSheetResult {
|
||||
final Set<String> statuses;
|
||||
final String? retState; // null=未选退单态
|
||||
const _StatusSheetResult(this.statuses, this.retState);
|
||||
}
|
||||
|
||||
class _StatusMultiSheet extends StatefulWidget {
|
||||
final Set<String> initialStatuses;
|
||||
final String? initialRetState;
|
||||
const _StatusMultiSheet({
|
||||
super.key,
|
||||
required this.initialStatuses,
|
||||
required this.initialRetState,
|
||||
});
|
||||
|
||||
@override
|
||||
State<_StatusMultiSheet> createState() => _StatusMultiSheetState();
|
||||
}
|
||||
|
||||
class _StatusMultiSheetState extends State<_StatusMultiSheet> {
|
||||
late Set<String> _sel;
|
||||
String? _ret;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_sel = Set.of(widget.initialStatuses);
|
||||
_ret = widget.initialRetState;
|
||||
}
|
||||
|
||||
/// 重置(sheet actions「重置」调用):回默认「草稿+待审核」(D3)。
|
||||
void reset() {
|
||||
setState(() {
|
||||
_sel = Set.of(_StockOutListScreenState._defaultStatuses);
|
||||
_ret = null;
|
||||
});
|
||||
}
|
||||
|
||||
/// 确定(sheet actions「确定」调用):组装结果并关闭 sheet。
|
||||
void apply() {
|
||||
Navigator.of(context).pop(_StatusSheetResult(_sel, _ret));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final t = context.tokens;
|
||||
final mainCodes = _StockOutListScreenState._mainCodes;
|
||||
final mainLabels = _StockOutListScreenState._mainLabels;
|
||||
final retCodes = _StockOutListScreenState._retCodes;
|
||||
final retLabels = _StockOutListScreenState._retLabels;
|
||||
|
||||
Widget row(String code, String label, {required bool isRet}) {
|
||||
final sel = isRet ? _ret == code : _sel.contains(code);
|
||||
return InkWell(
|
||||
onTap: () => setState(() {
|
||||
if (isRet) {
|
||||
_ret = _ret == code ? null : code;
|
||||
_sel = {};
|
||||
} else {
|
||||
final next = Set.of(_sel);
|
||||
next.contains(code) ? next.remove(code) : next.add(code);
|
||||
_sel = next;
|
||||
_ret = null;
|
||||
}
|
||||
}),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 13, horizontal: 4),
|
||||
decoration:
|
||||
BoxDecoration(border: Border(bottom: BorderSide(color: t.borderSubtle))),
|
||||
child: Row(children: [
|
||||
DsIconBadge(label),
|
||||
const SizedBox(width: 10),
|
||||
Text(label,
|
||||
style: TextStyle(
|
||||
fontSize: AppDims.fsBody,
|
||||
fontWeight: sel ? FontWeight.w600 : FontWeight.w400,
|
||||
color: sel ? t.primary : t.text)),
|
||||
const Spacer(),
|
||||
if (sel) Icon(LucideIcons.check, size: 18, color: t.primary),
|
||||
]),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
for (final c in mainCodes) row(c, mainLabels[c]!, isRet: false),
|
||||
for (final r in retCodes) row(r, retLabels[r]!, isRet: true),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ── 详细搜索模态框 ────────────────────────────────────────────────
|
||||
|
||||
/// 详细搜索结果值(提交回父屏一次性应用)。
|
||||
|
||||
|
Before Width: | Height: | Size: 148 KiB After Width: | Height: | Size: 149 KiB |
|
Before Width: | Height: | Size: 147 KiB After Width: | Height: | Size: 148 KiB |
|
Before Width: | Height: | Size: 150 KiB After Width: | Height: | Size: 151 KiB |
|
Before Width: | Height: | Size: 144 KiB After Width: | Height: | Size: 145 KiB |
|
Before Width: | Height: | Size: 143 KiB After Width: | Height: | Size: 143 KiB |
|
Before Width: | Height: | Size: 146 KiB After Width: | Height: | Size: 146 KiB |
|
Before Width: | Height: | Size: 304 KiB After Width: | Height: | Size: 309 KiB |
|
Before Width: | Height: | Size: 300 KiB After Width: | Height: | Size: 303 KiB |
|
Before Width: | Height: | Size: 305 KiB After Width: | Height: | Size: 311 KiB |
|
Before Width: | Height: | Size: 140 KiB After Width: | Height: | Size: 140 KiB |
|
Before Width: | Height: | Size: 138 KiB After Width: | Height: | Size: 139 KiB |
|
Before Width: | Height: | Size: 141 KiB After Width: | Height: | Size: 142 KiB |
|
Before Width: | Height: | Size: 300 KiB After Width: | Height: | Size: 302 KiB |
|
Before Width: | Height: | Size: 294 KiB After Width: | Height: | Size: 298 KiB |
|
Before Width: | Height: | Size: 301 KiB After Width: | Height: | Size: 302 KiB |
|
Before Width: | Height: | Size: 138 KiB After Width: | Height: | Size: 139 KiB |
|
Before Width: | Height: | Size: 137 KiB After Width: | Height: | Size: 138 KiB |
|
Before Width: | Height: | Size: 140 KiB After Width: | Height: | Size: 140 KiB |