diff --git a/client/lib/providers/stock_in_provider.dart b/client/lib/providers/stock_in_provider.dart index e85d4e5..9d8f24c 100644 --- a/client/lib/providers/stock_in_provider.dart +++ b/client/lib/providers/stock_in_provider.dart @@ -38,7 +38,8 @@ final stockInSummary30Provider = class StockInListNotifier extends AsyncNotifier> { int _page = 1; int _pageSize = AppConstants.defaultPageSize; - String _status = ''; + // 默认「草稿+待审核」(2026-07-14 用户拍板,状态筛选改多选后的默认态)。 + String _status = 'draft,pending'; String? _startDate; String? _endDate; String _keyword = ''; diff --git a/client/lib/providers/stock_out_provider.dart b/client/lib/providers/stock_out_provider.dart index 5fcf3b6..65a18e2 100644 --- a/client/lib/providers/stock_out_provider.dart +++ b/client/lib/providers/stock_out_provider.dart @@ -38,7 +38,8 @@ final stockOutSummary30Provider = class StockOutListNotifier extends AsyncNotifier> { int _page = 1; int _pageSize = AppConstants.defaultPageSize; - String _status = ''; + // 默认「草稿+待审核」(2026-07-14 用户拍板,状态筛选改多选后的默认态)。 + String _status = 'draft,pending'; String? _startDate; String? _endDate; String _keyword = ''; diff --git a/client/lib/screens/stock_in/stock_in_list_screen.dart b/client/lib/screens/stock_in/stock_in_list_screen.dart index 5ebe583..cf63048 100644 --- a/client/lib/screens/stock_in/stock_in_list_screen.dart +++ b/client/lib/screens/stock_in/stock_in_list_screen.dart @@ -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 { final _searchCtrl = TextEditingController(); - String _statusFilter = ''; // '' = 全部 + // 状态筛选 2026-07-14 改多选:主状态(草稿/待审核/已审核/已拒绝)多选,空集=全部; + // 默认「草稿+待审核」(D3 用户拍板)。退单伪状态走 _detail['return_state'],与主状态互斥(D4)。 + Set _statusFilter = {'draft', 'pending'}; DateTimeRange? _dateRange; String _datePresetLabel = ''; // 入库时间 chip 显示的预设名(近7天/本月/自定义…) String? _warehouseName; // 仓库客户端筛选(单选) - // 详细搜索多字段(含 partner_id,与工具栏供应商联动)。key 用后端参数名。 + // 详细搜索多字段(含 partner_id,与工具栏供应商联动;return_state 也走这里)。key 用后端参数名。 Map _detail = const {}; // 表格列(照原型顺序,固定列,无列设置)。 @@ -73,25 +76,28 @@ class _StockInListScreenState extends ConsumerState { ('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 { 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.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 { ? '${_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 { } } + /// 覆盖式设置单一状态(D5:KPI 卡点击覆盖为该单值,不是 toggle 集合成员); + /// `s` 为空表示「全部」,`ret:` 前缀表示退单伪状态(与主状态互斥)。 void _setStatus(String s) { - setState(() => _statusFilter = s); final n = ref.read(stockInListProvider.notifier); final d = Map.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.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( + 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.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.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.of(_detail); if (id == null) { @@ -185,7 +283,7 @@ class _StockInListScreenState extends ConsumerState { void _resetFilters() { setState(() { _searchCtrl.clear(); - _statusFilter = ''; + _statusFilter = Set.of(_defaultStatuses); // D3:回默认「草稿+待审核」,不是回「全部」 _dateRange = null; _datePresetLabel = ''; _warehouseName = null; @@ -193,7 +291,7 @@ class _StockInListScreenState extends ConsumerState { }); 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 { ), ); + /// 详细搜索的「状态」字段仍单选(未扩展为多选):多选态无法单值呈现时归「全部」。 + String get _statusForAdvDialog => + _statusFilter.length == 1 ? _statusFilter.first : ''; + Future _openAdvSearch() async { final _AdvResult? res; if (context.isMobile) { @@ -281,7 +383,7 @@ class _StockInListScreenState extends ConsumerState { builder: (_) => _AdvSearchSheet( key: formKey, initialDetail: _detail, - initialStatus: _statusFilter, + initialStatus: _statusForAdvDialog, initialRange: _dateRange, ), actions: [ @@ -296,7 +398,7 @@ class _StockInListScreenState extends ConsumerState { context: context, builder: (_) => _AdvSearchDialog( initialDetail: _detail, - initialStatus: _statusFilter, + initialStatus: _statusForAdvDialog, initialRange: _dateRange, ), ); @@ -305,19 +407,20 @@ class _StockInListScreenState extends ConsumerState { 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 { 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 { 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 { ); } - /// 状态筛选底部 sheet(原型 openStatusSheet 的 m-opt 列表:图标 + 状态词 + 勾)。 + /// 状态筛选底部 sheet(多选勾选 + 确定应用,D5/D4):主状态多选 + 退单伪状态互斥。 Future _openStatusSheet() async { - final sel = await showMSheet( + 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.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 { onClear: () => setState(() => _warehouseName = null)), ); - final statusChip = PopupMenuButton( - 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 { } } +// ── 状态筛选底部 sheet(原型 openStatusSheet 多选版):主状态 checkbox 多选 + +// 退单伪状态互斥单选,底部「重置/确定」应用(对齐 D3/D4/Step5)───────────── +class _StatusSheetResult { + final Set statuses; + final String? retState; // null=未选退单态 + const _StatusSheetResult(this.statuses, this.retState); +} + +class _StatusMultiSheet extends StatefulWidget { + final Set 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 _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; diff --git a/client/lib/screens/stock_out/stock_out_list_screen.dart b/client/lib/screens/stock_out/stock_out_list_screen.dart index c2577bc..cc16cdb 100644 --- a/client/lib/screens/stock_out/stock_out_list_screen.dart +++ b/client/lib/screens/stock_out/stock_out_list_screen.dart @@ -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 { final _searchCtrl = TextEditingController(); - // 状态('' = 全部)+ 日期区间:走 setStatus / setDateRange。 - String _statusFilter = ''; + // 状态筛选 2026-07-14 改多选:主状态(草稿/待审核/已审核/已拒绝)多选,空集=全部; + // 默认「草稿+待审核」(D3 用户拍板)。退单伪状态仍走 _returnState,与主状态互斥(D4)。 + Set _statusFilter = {'draft', 'pending'}; DateTimeRange? _dateRange; String _appliedDateKey = ''; String _datePresetLabel = ''; // 出库时间 chip 显示的预设名 @@ -80,23 +82,24 @@ class _StockOutListScreenState extends ConsumerState { 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 { 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 { _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 { 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 { 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 { void _resetFilters() { setState(() { _searchCtrl.clear(); - _statusFilter = ''; + _statusFilter = Set.of(_defaultStatuses); // D3:回默认「草稿+待审核」,不是回「全部」 _dateRange = null; _appliedDateKey = ''; _datePresetLabel = ''; @@ -223,7 +242,7 @@ class _StockOutListScreenState extends ConsumerState { }); 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 { 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 { 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 { // ── 窄屏形态(对齐移动原型 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( + 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 { 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 { _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 { 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 { ); } - /// 状态筛选底部 sheet(原型 openStatusSheet 的 m-opt 列表:图标 + 状态词 + 勾)。 + /// 状态筛选底部 sheet(多选勾选 + 确定应用,D5/D4):主状态多选 + 退单伪状态互斥。 Future _openStatusSheet() async { - final sel = await showMSheet( + 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 { 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 { // ── 详细搜索模态(11 字段)────────────────────────────────────── + /// 详细搜索的「状态」字段仍单选(未扩展为多选):多选态无法单值呈现时归「全部」。 + String get _statusForAdvDialog => + _statusFilter.length == 1 ? _statusFilter.first : ''; + Future _openAdvSearch() async { final initial = _AdvResult( orderNo: _orderNo, @@ -932,7 +968,7 @@ class _StockOutListScreenState extends ConsumerState { spec: _spec, operatorId: _operatorId, reviewerId: _reviewerId, - status: _statusFilter, + status: _statusForAdvDialog, dateRange: _dateRange, ); final _AdvResult? result; @@ -968,7 +1004,7 @@ class _StockOutListScreenState extends ConsumerState { _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 { } } +// ── 状态筛选底部 sheet(原型 openStatusSheet 多选版):主状态 checkbox 多选 + +// 退单伪状态互斥单选,底部「重置/确定」应用(对齐 D3/D4/Step5)───────────── +class _StatusSheetResult { + final Set statuses; + final String? retState; // null=未选退单态 + const _StatusSheetResult(this.statuses, this.retState); +} + +class _StatusMultiSheet extends StatefulWidget { + final Set 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 _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), + ], + ); + } +} + // ── 详细搜索模态框 ──────────────────────────────────────────────── /// 详细搜索结果值(提交回父屏一次性应用)。 diff --git a/client/test/golden/goldens/m_stock_in_list_a.png b/client/test/golden/goldens/m_stock_in_list_a.png index 23fe9a1..efb281e 100644 Binary files a/client/test/golden/goldens/m_stock_in_list_a.png and b/client/test/golden/goldens/m_stock_in_list_a.png differ diff --git a/client/test/golden/goldens/m_stock_in_list_b.png b/client/test/golden/goldens/m_stock_in_list_b.png index 80363f1..edcbc2b 100644 Binary files a/client/test/golden/goldens/m_stock_in_list_b.png and b/client/test/golden/goldens/m_stock_in_list_b.png differ diff --git a/client/test/golden/goldens/m_stock_in_list_c.png b/client/test/golden/goldens/m_stock_in_list_c.png index 127dadc..e829b52 100644 Binary files a/client/test/golden/goldens/m_stock_in_list_c.png and b/client/test/golden/goldens/m_stock_in_list_c.png differ diff --git a/client/test/golden/goldens/m_stock_out_list_a.png b/client/test/golden/goldens/m_stock_out_list_a.png index 68f4c89..cacb970 100644 Binary files a/client/test/golden/goldens/m_stock_out_list_a.png and b/client/test/golden/goldens/m_stock_out_list_a.png differ diff --git a/client/test/golden/goldens/m_stock_out_list_b.png b/client/test/golden/goldens/m_stock_out_list_b.png index 54664f3..5462db2 100644 Binary files a/client/test/golden/goldens/m_stock_out_list_b.png and b/client/test/golden/goldens/m_stock_out_list_b.png differ diff --git a/client/test/golden/goldens/m_stock_out_list_c.png b/client/test/golden/goldens/m_stock_out_list_c.png index c2cadbb..70f51bc 100644 Binary files a/client/test/golden/goldens/m_stock_out_list_c.png and b/client/test/golden/goldens/m_stock_out_list_c.png differ diff --git a/client/test/golden/goldens/stock_in_list_a.png b/client/test/golden/goldens/stock_in_list_a.png index c8f6494..178e5ac 100644 Binary files a/client/test/golden/goldens/stock_in_list_a.png and b/client/test/golden/goldens/stock_in_list_a.png differ diff --git a/client/test/golden/goldens/stock_in_list_b.png b/client/test/golden/goldens/stock_in_list_b.png index e1fe1db..b918289 100644 Binary files a/client/test/golden/goldens/stock_in_list_b.png and b/client/test/golden/goldens/stock_in_list_b.png differ diff --git a/client/test/golden/goldens/stock_in_list_c.png b/client/test/golden/goldens/stock_in_list_c.png index f2a5b1f..aecd875 100644 Binary files a/client/test/golden/goldens/stock_in_list_c.png and b/client/test/golden/goldens/stock_in_list_c.png differ diff --git a/client/test/golden/goldens/stock_in_list_mobile_a.png b/client/test/golden/goldens/stock_in_list_mobile_a.png index ec97077..c47a134 100644 Binary files a/client/test/golden/goldens/stock_in_list_mobile_a.png and b/client/test/golden/goldens/stock_in_list_mobile_a.png differ diff --git a/client/test/golden/goldens/stock_in_list_mobile_b.png b/client/test/golden/goldens/stock_in_list_mobile_b.png index adce576..000e127 100644 Binary files a/client/test/golden/goldens/stock_in_list_mobile_b.png and b/client/test/golden/goldens/stock_in_list_mobile_b.png differ diff --git a/client/test/golden/goldens/stock_in_list_mobile_c.png b/client/test/golden/goldens/stock_in_list_mobile_c.png index 4eba4f9..47af963 100644 Binary files a/client/test/golden/goldens/stock_in_list_mobile_c.png and b/client/test/golden/goldens/stock_in_list_mobile_c.png differ diff --git a/client/test/golden/goldens/stock_out_list_a.png b/client/test/golden/goldens/stock_out_list_a.png index 305fd11..680a3a2 100644 Binary files a/client/test/golden/goldens/stock_out_list_a.png and b/client/test/golden/goldens/stock_out_list_a.png differ diff --git a/client/test/golden/goldens/stock_out_list_b.png b/client/test/golden/goldens/stock_out_list_b.png index ebe0f10..e005b4e 100644 Binary files a/client/test/golden/goldens/stock_out_list_b.png and b/client/test/golden/goldens/stock_out_list_b.png differ diff --git a/client/test/golden/goldens/stock_out_list_c.png b/client/test/golden/goldens/stock_out_list_c.png index 69f8dba..7f93579 100644 Binary files a/client/test/golden/goldens/stock_out_list_c.png and b/client/test/golden/goldens/stock_out_list_c.png differ diff --git a/client/test/golden/goldens/stock_out_list_mobile_a.png b/client/test/golden/goldens/stock_out_list_mobile_a.png index bcff332..8dc2c86 100644 Binary files a/client/test/golden/goldens/stock_out_list_mobile_a.png and b/client/test/golden/goldens/stock_out_list_mobile_a.png differ diff --git a/client/test/golden/goldens/stock_out_list_mobile_b.png b/client/test/golden/goldens/stock_out_list_mobile_b.png index 9e8c625..7482f58 100644 Binary files a/client/test/golden/goldens/stock_out_list_mobile_b.png and b/client/test/golden/goldens/stock_out_list_mobile_b.png differ diff --git a/client/test/golden/goldens/stock_out_list_mobile_c.png b/client/test/golden/goldens/stock_out_list_mobile_c.png index 3d6d420..f53750f 100644 Binary files a/client/test/golden/goldens/stock_out_list_mobile_c.png and b/client/test/golden/goldens/stock_out_list_mobile_c.png differ diff --git a/design/prototype/screens/m-stock-in-list.html b/design/prototype/screens/m-stock-in-list.html index 47e5f91..a275c7f 100644 --- a/design/prototype/screens/m-stock-in-list.html +++ b/design/prototype/screens/m-stock-in-list.html @@ -51,11 +51,15 @@ const ORDERS=[ {no:'RK-20260618-002',party:'泸州老窖仓',wh:'名酒仓',items:2,amount:'¥16,000',status:'已审核',date:'2026-06-18',by:'王经理',auditor:'王经理',goods:'泸州老窖 特曲、国窖 1573',lines:[{n:'泸州老窖 特曲',s:'500ml×6',q:25,a:'¥8,000',pd:'2022-02-14',b:'20260415'},{n:'国窖 1573',s:'500ml×6',q:8,a:'¥8,000',pd:'2021-04-09',b:'20260410'}]}, {no:'RK-20260618-001',party:'洋河股份',wh:'主仓',items:2,amount:'¥14,800',status:'草稿',date:'2026-06-18',by:'王经理',auditor:'',goods:'洋河 梦之蓝 M6、洋河 海之蓝',lines:[{n:'洋河 梦之蓝 M6',s:'500ml×6',q:12,a:'¥7,440',pd:'2020-12-01',b:'20260322'},{n:'洋河 海之蓝',s:'500ml×6',q:16,a:'¥7,360',pd:'2022-06-11',b:'20260305'}]}, ]; -/* 状态词与桌面/后端对齐:主状态 4 词 + 退单态 2 词(return_state: partial/full) */ -const ST=['全部','草稿','待审核','已审核','已拒绝','部分退单','已退单']; +/* 状态词与桌面/后端对齐:主状态 4 词 + 退单态 2 词(return_state: partial/full)。 + 2026-07-14 改多选:主状态多选、退单伪状态与主状态互斥,默认「草稿+待审核」(D3 用户拍板)。 */ +const MAIN_ST=['草稿','待审核','已审核','已拒绝']; +const RET_ST=['部分退单','已退单']; +const DEFAULT_ST=['草稿','待审核']; +const ST=['全部',...MAIN_ST,...RET_ST]; const OPS=['全部','王经理','李采购']; const REVIEWERS=['全部','张主管','王经理']; -const state={q:'',status:'全部'}; +const state={q:'',statusSet:new Set(DEFAULT_ST),ret:''}; /* ---- 状态代表图标(移动纯图标 .ico / 图标+文字 .bi;状态筛选钮为纯文字——用户拍板) ---- */ const STICON={'草稿':'i-ic06','待审核':'i-ic04','已审核':'i-check','已拒绝':'i-close','部分退单':'i-undo','已退单':'i-undo','待定价':'i-yen'}; function icoB(v){ return ``; } @@ -66,8 +70,8 @@ let adv={...ADV_DEF}; function advCount(){ return Object.keys(ADV_DEF).filter(k=>adv[k]!==ADV_DEF[k]).length; } function filtered(){ return ORDERS.filter(o=>{ - const st=state.status; - if(st!=='全部'&&o.status!==st&&o.ret!==st)return false; + if(state.ret){ if(o.ret!==state.ret)return false; } + else if(state.statusSet.size&&!state.statusSet.has(o.status))return false; if(state.q){const q=state.q.toLowerCase(); if(!o.no.toLowerCase().includes(q)&&!o.party.toLowerCase().includes(q)&&!(o.goods||'').toLowerCase().includes(q))return false;} if(adv.no&&!o.no.toLowerCase().includes(adv.no.toLowerCase()))return false; if(adv.product){const p=adv.product.toLowerCase(); if(!(o.lines||[]).some(l=>l.n.toLowerCase().includes(p)))return false;} @@ -76,11 +80,20 @@ function filtered(){ return ORDERS.filter(o=>{ if(adv.reviewer!=='全部'&&o.auditor!==adv.reviewer)return false; if(adv.status!=='全部'&&o.status!==adv.status&&o.ret!==adv.status)return false; return true; }); } +// 状态摘要:无=全部、1 项=值本身、多项=「N 项」;退单态优先展示。 +function statusLabel(){ + if(state.ret) return state.ret; + if(!state.statusSet.size) return '全部'; + if(state.statusSet.size===1) return [...state.statusSet][0]; + return state.statusSet.size+' 项'; +} +// 是否为「全部」(空集且无退单)——状态钮高亮判断:非「全部」即视为已筛选(含默认态)。 +function isAllStatus(){ return !state.ret && !state.statusSet.size; } function render(){ const n=advCount(); - document.getElementById('stVal').textContent=state.status; + document.getElementById('stVal').textContent=statusLabel(); const ss=document.getElementById('stSel'); - ss.classList.toggle('primary',state.status!=='全部'); ss.classList.toggle('ghost',state.status==='全部'); + ss.classList.toggle('primary',!isAllStatus()); ss.classList.toggle('ghost',isAllStatus()); const ab=document.getElementById('advBtn'); ab.classList.toggle('primary',n>0); ab.classList.toggle('ghost',n===0); const rows=filtered(); @@ -96,11 +109,35 @@ function render(){
${o.date} · 经手人 ${o.by}${o.auditor?' · 审核 '+o.auditor:''}
`).join(''); } -function setSt(s){ state.status=s; render(); } -/* ---- 状态下拉(底部选项 sheet) ---- */ -function openStatusSheet(){ - openSheet('状态筛选', ST.map(s=>`
${s!=='全部'?``:''}${s}
`).join('')); +// KPI 卡点击 = 覆盖为该单值(D5,非 toggle 集合成员);'全部' 清空。 +function setSt(s){ + if(s==='全部'){ state.statusSet=new Set(); state.ret=''; } + else if(RET_ST.includes(s)){ state.ret=s; state.statusSet=new Set(); } + else { state.ret=''; state.statusSet=new Set([s]); } + render(); } +/* ---- 状态筛选(底部 sheet,多选勾选 + 确定应用):主状态多选 + 退单伪状态互斥 ---- */ +let stDraft={sel:new Set(DEFAULT_ST),ret:''}; +function openStatusSheet(){ + stDraft={sel:new Set(state.statusSet),ret:state.ret}; + drawStatusSheet(); +} +function stOptRow(s,isRet){ + const sel = isRet? stDraft.ret===s : stDraft.sel.has(s); + return `
${s}
`; +} +function drawStatusSheet(){ + const rows = MAIN_ST.map(s=>stOptRow(s,false)).join('') + RET_ST.map(s=>stOptRow(s,true)).join(''); + openSheet('单据状态', rows+`
重置
确定
`); +} +function toggleStatusDraft(s,isRet){ + if(isRet){ stDraft.ret = stDraft.ret===s? '' : s; stDraft.sel=new Set(); } + else { stDraft.ret=''; stDraft.sel.has(s)?stDraft.sel.delete(s):stDraft.sel.add(s); } + drawStatusSheet(); +} +// 重置(D3):回到默认「草稿+待审核」,不是回「全部」。 +function resetStatusDraft(){ stDraft={sel:new Set(DEFAULT_ST),ret:''}; drawStatusSheet(); } +function applyStatusDraft(){ state.statusSet=new Set(stDraft.sel); state.ret=stDraft.ret; closeSheet(); render(); } /* ==================== 详细搜索(底部 sheet,竖排单列) ==================== */ let advOpen=''; // 当前展开的下拉字段 diff --git a/design/prototype/screens/m-stock-out-list.html b/design/prototype/screens/m-stock-out-list.html index 72fd5c8..61a338e 100644 --- a/design/prototype/screens/m-stock-out-list.html +++ b/design/prototype/screens/m-stock-out-list.html @@ -50,11 +50,15 @@ const ORDERS=[ {no:'CK-20260618-004',party:'城东烟酒行',wh:'主仓',items:2,amount:'¥12,500',profit:'¥1,870',status:'已审核',ret:'已退单',date:'2026-06-18',by:'王经理',auditor:'王经理',goods:'西凤酒 旗舰版、郎酒 红花郎 15',lines:[{n:'西凤酒 旗舰版',s:'500ml×6',q:13,a:'¥6,500',pd:'2021-12-05',b:'20260325'},{n:'郎酒 红花郎 15',s:'500ml×6',q:10,a:'¥6,000',pd:'2018-09-16',b:'20260118'}]}, {no:'CK-20260618-003',party:'金樽会所',wh:'名酒仓',items:1,amount:'¥8,400',profit:'¥1,260',status:'草稿',date:'2026-06-18',by:'李销售',auditor:'',goods:'汾酒 青花 20',lines:[{n:'汾酒 青花 20',s:'500ml×6',q:12,a:'¥8,400',pd:'2021-01-27',b:'20260506'}]}, ]; -/* 状态词与桌面/后端对齐:主状态 4 词 + 退单态 2 词(return_state: partial/full) */ -const ST=['全部','草稿','待审核','已审核','已拒绝','部分退单','已退单']; +/* 状态词与桌面/后端对齐:主状态 4 词 + 退单态 2 词(return_state: partial/full)。 + 2026-07-14 改多选:主状态多选、退单伪状态与主状态互斥,默认「草稿+待审核」(D3 用户拍板)。 */ +const MAIN_ST=['草稿','待审核','已审核','已拒绝']; +const RET_ST=['部分退单','已退单']; +const DEFAULT_ST=['草稿','待审核']; +const ST=['全部',...MAIN_ST,...RET_ST]; const OPS=['全部','王经理','李销售']; const REVIEWERS=['全部','张主管','王经理']; -const state={q:'',status:'全部'}; +const state={q:'',statusSet:new Set(DEFAULT_ST),ret:''}; /* ---- 状态代表图标(移动纯图标 .ico / 图标+文字 .bi;状态筛选钮为纯文字——用户拍板) ---- */ const STICON={'草稿':'i-ic06','待审核':'i-ic04','已审核':'i-check','已拒绝':'i-close','部分退单':'i-undo','已退单':'i-undo','待定价':'i-yen'}; function icoB(v){ return ``; } @@ -65,8 +69,8 @@ let adv={...ADV_DEF}; function advCount(){ return Object.keys(ADV_DEF).filter(k=>adv[k]!==ADV_DEF[k]).length; } function filtered(){ return ORDERS.filter(o=>{ - const st=state.status; - if(st!=='全部'&&o.status!==st&&o.ret!==st)return false; + if(state.ret){ if(o.ret!==state.ret)return false; } + else if(state.statusSet.size&&!state.statusSet.has(o.status))return false; if(state.q){const q=state.q.toLowerCase(); if(!o.no.toLowerCase().includes(q)&&!o.party.toLowerCase().includes(q)&&!(o.goods||'').toLowerCase().includes(q))return false;} if(adv.no&&!o.no.toLowerCase().includes(adv.no.toLowerCase()))return false; if(adv.product){const p=adv.product.toLowerCase(); if(!(o.lines||[]).some(l=>l.n.toLowerCase().includes(p)))return false;} @@ -75,11 +79,20 @@ function filtered(){ return ORDERS.filter(o=>{ if(adv.reviewer!=='全部'&&o.auditor!==adv.reviewer)return false; if(adv.status!=='全部'&&o.status!==adv.status&&o.ret!==adv.status)return false; return true; }); } +// 状态摘要:无=全部、1 项=值本身、多项=「N 项」;退单态优先展示。 +function statusLabel(){ + if(state.ret) return state.ret; + if(!state.statusSet.size) return '全部'; + if(state.statusSet.size===1) return [...state.statusSet][0]; + return state.statusSet.size+' 项'; +} +// 是否为「全部」(空集且无退单)——状态钮高亮判断:非「全部」即视为已筛选(含默认态)。 +function isAllStatus(){ return !state.ret && !state.statusSet.size; } function render(){ const n=advCount(); - document.getElementById('stVal').textContent=state.status; + document.getElementById('stVal').textContent=statusLabel(); const ss=document.getElementById('stSel'); - ss.classList.toggle('primary',state.status!=='全部'); ss.classList.toggle('ghost',state.status==='全部'); + ss.classList.toggle('primary',!isAllStatus()); ss.classList.toggle('ghost',isAllStatus()); const ab=document.getElementById('advBtn'); ab.classList.toggle('primary',n>0); ab.classList.toggle('ghost',n===0); const rows=filtered(); @@ -95,11 +108,35 @@ function render(){
${o.date} · 经手人 ${o.by}${o.auditor?' · 审核 '+o.auditor:''}
`).join(''); } -function setSt(s){ state.status=s; render(); } -/* ---- 状态下拉(底部选项 sheet) ---- */ -function openStatusSheet(){ - openSheet('状态筛选', ST.map(s=>`
${s!=='全部'?``:''}${s}
`).join('')); +// KPI 卡点击 = 覆盖为该单值(D5,非 toggle 集合成员);'全部' 清空。 +function setSt(s){ + if(s==='全部'){ state.statusSet=new Set(); state.ret=''; } + else if(RET_ST.includes(s)){ state.ret=s; state.statusSet=new Set(); } + else { state.ret=''; state.statusSet=new Set([s]); } + render(); } +/* ---- 状态筛选(底部 sheet,多选勾选 + 确定应用):主状态多选 + 退单伪状态互斥 ---- */ +let stDraft={sel:new Set(DEFAULT_ST),ret:''}; +function openStatusSheet(){ + stDraft={sel:new Set(state.statusSet),ret:state.ret}; + drawStatusSheet(); +} +function stOptRow(s,isRet){ + const sel = isRet? stDraft.ret===s : stDraft.sel.has(s); + return `
${s}
`; +} +function drawStatusSheet(){ + const rows = MAIN_ST.map(s=>stOptRow(s,false)).join('') + RET_ST.map(s=>stOptRow(s,true)).join(''); + openSheet('单据状态', rows+`
重置
确定
`); +} +function toggleStatusDraft(s,isRet){ + if(isRet){ stDraft.ret = stDraft.ret===s? '' : s; stDraft.sel=new Set(); } + else { stDraft.ret=''; stDraft.sel.has(s)?stDraft.sel.delete(s):stDraft.sel.add(s); } + drawStatusSheet(); +} +// 重置(D3):回到默认「草稿+待审核」,不是回「全部」。 +function resetStatusDraft(){ stDraft={sel:new Set(DEFAULT_ST),ret:''}; drawStatusSheet(); } +function applyStatusDraft(){ state.statusSet=new Set(stDraft.sel); state.ret=stDraft.ret; closeSheet(); render(); } /* ==================== 详细搜索(底部 sheet,竖排单列) ==================== */ let advOpen=''; diff --git a/design/prototype/screens/stock-in-list.html b/design/prototype/screens/stock-in-list.html index c3fed2e..ab2e4b2 100644 --- a/design/prototype/screens/stock-in-list.html +++ b/design/prototype/screens/stock-in-list.html @@ -24,6 +24,11 @@ .toolbar .daterange svg{width:14px; height:14px; stroke-width:1.8; color:var(--faint);} .toolbar .daterange .dv{color:var(--text);} .toolbar .sp{margin-left:auto;} + /* chip 尾部图标切换:未筛选=下拉箭头,筛选中=× 单清(镜像 inventory.html 多选 chip,待提升至 atoms.css) */ + .chip .cx{display:none;} + .chip.on .cx{display:block; cursor:pointer; color:var(--muted);} + .chip.on .cx:hover{color:var(--text);} + .chip.on .caret{display:none;} /* ---- 状态徽章(新增变体,待提升至 atoms.css) ---- */ .badge.b-草稿{background:var(--info-soft); color:var(--muted);} @@ -64,7 +69,7 @@
供应商
仓库
-
状态
+
状态
入库时间
详细搜索
@@ -144,9 +149,14 @@ const ORDERS=[ {no:'RK-20260613-001',date:'2026-06-13',party:'西凤酒陕西总代',wh:'主仓',count:1,amount:6720,status:'草稿',op:'张主管',auditor:'',lines:[{name:'西凤酒 旗舰版',qty:24,price:280}]}, {no:'RK-20260612-003',date:'2026-06-12',party:'水井坊川渝仓',wh:'名酒仓',count:1,amount:16320,status:'待审核',op:'王经理',auditor:'',lines:[{name:'水井坊 井台',qty:24,price:680}]}, ]; -const STATUS=['全部','草稿','待审核','已审核','已拒绝','部分退单','已退单']; +// 状态筛选 2026-07-14 改多选:主状态(草稿/待审核/已审核/已拒绝)可多选,退单伪状态(部分退单/已退单) +// 与主状态互斥(选其一清另一侧)。默认「草稿+待审核」(D3 用户拍板)。 +const MAIN_STATUS=['草稿','待审核','已审核','已拒绝']; +const RET_STATUS=['部分退单','已退单']; +const DEFAULT_STATUS=['草稿','待审核']; +const STATUS=['全部',...MAIN_STATUS,...RET_STATUS]; // 供详细搜索单选字段沿用 const COLS=[{key:'no',label:'单号'},{key:'party',label:'供应商'},{key:'wh',label:'仓库'},{key:'amount',label:'合计金额',num:true},{key:'status',label:'状态'},{key:'op',label:'入库员'},{key:'auditor',label:'审核员'},{key:'act',label:'操作',fixed:true}]; -const state={q:'',party:'全部',wh:'全部',status:'全部',dateFrom:'',dateTo:'',page:1,perPage:10,orderNo:'',productInfo:'',batch:'',series:'全部',spec:'全部',category:'全部',op:'全部',reviewer:'全部'}; +const state={q:'',party:'全部',wh:'全部',statusSet:new Set(DEFAULT_STATUS),ret:'',dateFrom:'',dateTo:'',page:1,perPage:10,orderNo:'',productInfo:'',batch:'',series:'全部',spec:'全部',category:'全部',op:'全部',reviewer:'全部'}; const PARTIES=['全部',...Array.from(new Set(ORDERS.map(o=>o.party)))]; const WAREHOUSES=['全部',...Array.from(new Set(ORDERS.map(o=>o.wh)))]; @@ -159,7 +169,8 @@ const STICON={'草稿':'i-ic06','待审核':'i-ic04','已审核':'i-check','已 function stBadge(v){ return `${v}`; } function numf(n){ return (Number(n)||0).toLocaleString(); } /* 明细行内数字:去¥,缩一号(2026-07-03) */ function filtered(){ return ORDERS.filter(o=>{ - if(state.status!=='全部'&&o.status!==state.status&&o.ret!==state.status)return false; + if(state.ret){ if(o.ret!==state.ret)return false; } + else if(state.statusSet.size&&!state.statusSet.has(o.status))return false; if(state.party!=='全部'&&o.party!==state.party)return false; if(state.wh!=='全部'&&o.wh!==state.wh)return false; if(state.dateFrom&&o.date{ if(state.category&&state.category!=='全部'&&!L.some(l=>(PINFO[l.name]||{}).cat===state.category))return false; if(state.batch){const b=state.batch.toLowerCase(); if(!L.some(l=>{const pi=PINFO[l.name]||{}; return (pi.batch||'').toLowerCase().includes(b)||(pi.code||'').toLowerCase().includes(b);}))return false;} return true; }); } +// 状态 chip 摘要(镜像 inventory.html 多选 chip):无=空、1 项=值本身、多项=「N 项」;退单态优先展示。 +function statusChipLabel(){ + if(state.ret) return state.ret; + if(!state.statusSet.size) return ''; + if(state.statusSet.size===1) return [...state.statusSet][0]; + return state.statusSet.size+' 项'; +} +// 状态是否为默认态(草稿+待审核、无退单)——仅用于「重置」按钮高亮判断,避免默认态常年高亮。 +function isDefaultStatus(){ return !state.ret && state.statusSet.size===2 && state.statusSet.has('草稿') && state.statusSet.has('待审核'); } function syncChips(){ const set=(id,chipId,val)=>{ document.getElementById(id).textContent=val||''; document.getElementById(chipId).classList.toggle('on',!!val); }; set('partyVal','chipParty',state.party==='全部'?'':state.party); set('whVal','chipWh',state.wh==='全部'?'':state.wh); - set('statusVal','chipStatus',state.status==='全部'?'':state.status); + set('statusVal','chipStatus',statusChipLabel()); set('dateVal','chipDate',state.dateLabel||''); const advCount=[state.orderNo,state.productInfo,state.batch, (state.series&&state.series!=='全部')?1:0, @@ -192,13 +212,13 @@ function syncChips(){ advBtnEl.classList.toggle('primary',advCount>0); advBtnEl.classList.toggle('soft',advCount===0); document.getElementById('advCount').textContent=advCount?' · '+advCount:''; - const any=state.q||state.party!=='全部'||state.wh!=='全部'||state.status!=='全部'||state.dateFrom||state.dateTo||advCount>0; + const any=state.q||state.party!=='全部'||state.wh!=='全部'||!isDefaultStatus()||state.dateFrom||state.dateTo||advCount>0; document.getElementById('resetBtn').classList.toggle('primary',!!any); } function render(){ syncChips(); const cols=COLS; - document.getElementById('thead').innerHTML=''+cols.map(c=>{ const cls=(c.num?'num ':'')+(c.key==='act'?'act ':''); if(c.filter){const on=state.status!=='全部'; return `${c.label} `;} return `${c.label}`; }).join('')+''; + document.getElementById('thead').innerHTML=''+cols.map(c=>{ const cls=(c.num?'num ':'')+(c.key==='act'?'act ':''); if(c.filter){const on=!isDefaultStatus(); return `${c.label} `;} return `${c.label}`; }).join('')+''; const rows=filtered(); const start=(state.page-1)*state.perPage; const pageRows=rows.slice(start,start+state.perPage); const tb=document.getElementById('tbody'); if(rows.length===0){ tb.innerHTML=`
没有匹配的入库单 · 试试调整筛选或搜索
`; } else { tb.innerHTML=pageRows.map(o=>{ const idx=ORDERS.indexOf(o); return ''+cols.map(c=>{ if(c.key==='no')return `${o.no}`; if(c.key==='party')return `${o.party}`; if(c.key==='wh')return `${o.wh}`; if(c.key==='amount')return `${money(o.amount)}`; if(c.key==='status')return `${stBadge(o.status)}${o.ret?' '+stBadge(o.ret):''}`; if(c.key==='op')return `${o.op}`; if(c.key==='auditor')return `${o.auditor?o.auditor:''}`; if(c.key==='act')return `
`; return `${o[c.key]}`; }).join('')+''; }).join(''); } @@ -210,10 +230,32 @@ function go(p){ const pages=Math.max(1,Math.ceil(filtered().length/state.perPage const PAGE_SIZES=[10,20,50,100]; function openPageSize(e){ openMenu(e.currentTarget, PAGE_SIZES.map(n=>({v:String(n),label:String(n),sel:n===state.perPage})), v=>{ closeMenus(); setPerPage(v); }, 'pgsize-menu', 88); } function setPerPage(v){ state.perPage=+v; state.page=1; const el=document.getElementById('pageSizeVal'); if(el)el.textContent=v; render(); } -function filterStatus(s){ state.status=s; state.page=1; render(); } -function clearFilters(){ state.q='';state.party='全部';state.wh='全部';state.status='全部';state.dateFrom='';state.dateTo='';state.dateLabel='';state.datePreset='all';state.orderNo='';state.productInfo='';state.batch='';state.series='全部';state.spec='全部';state.category='全部';state.op='全部';state.reviewer='全部';state.page=1; document.getElementById('searchInput').value=''; render(); } +// KPI 卡点击 = 覆盖为该单值(D5,非 toggle 集合成员)。 +function filterStatus(s){ + if(RET_STATUS.includes(s)){ state.ret=s; state.statusSet=new Set(); } + else { state.ret=''; state.statusSet=new Set([s]); } + state.page=1; render(); +} +// 状态 chip 的 × 单清:回到默认「草稿+待审核」,不是回「全部」(D3)。 +function clearStatus(e){ e.stopPropagation(); state.statusSet=new Set(DEFAULT_STATUS); state.ret=''; state.page=1; render(); } +function clearFilters(){ state.q='';state.party='全部';state.wh='全部';state.statusSet=new Set(DEFAULT_STATUS);state.ret='';state.dateFrom='';state.dateTo='';state.dateLabel='';state.datePreset='all';state.orderNo='';state.productInfo='';state.batch='';state.series='全部';state.spec='全部';state.category='全部';state.op='全部';state.reviewer='全部';state.page=1; document.getElementById('searchInput').value=''; render(); } -function openStatusFilter(t){ openMenu(t,STATUS.map(v=>({v,label:v,sel:v===state.status})),v=>{ state.status=v; state.page=1; closeMenus(); render(); }); } +// 状态多选菜单:主状态 checkbox 多选 + 退单伪状态互斥(选其一清主状态多选,反之亦然); +// 菜单勾选后不关闭(镜像 inventory.html openDimFilter 的重开手法)。 +function openStatusFilter(t){ + const opts=[...MAIN_STATUS.map(v=>({v,label:v,sel:state.statusSet.has(v)})), + ...RET_STATUS.map(v=>({v,label:v,sel:state.ret===v}))]; + openMenu(t,opts,v=>{ + if(RET_STATUS.includes(v)){ + state.ret = state.ret===v ? '' : v; + state.statusSet=new Set(); + } else { + state.ret=''; + state.statusSet.has(v)?state.statusSet.delete(v):state.statusSet.add(v); + } + state.page=1; render(); openStatusFilter(t); + }); +} function openPartyFilter(t){ openSearchMenu(t,PARTIES.map(v=>({v,label:v,sel:v===state.party})),v=>{ state.party=v; state.page=1; closeMenus(); render(); },{placeholder:'搜索供应商…'}); } function openWhFilter(t){ openMenu(t,WAREHOUSES.map(v=>({v,label:v,sel:v===state.wh})),v=>{ state.wh=v; state.page=1; closeMenus(); render(); }); } const TODAY='2026-06-22'; @@ -264,8 +306,14 @@ const OP_OPTS=['全部',...Array.from(new Set(ORDERS.map(o=>o.op)))]; const REVIEWER_OPTS=['全部',...Array.from(new Set(ORDERS.map(o=>o.auditor).filter(Boolean)))]; let adv={}; function advSet(id,val){ const el=document.getElementById(id); if(el)el.value=(!val||val==='全部')?'':val; } +// 详细搜索的「状态」字段仍是单选(原型/Flutter 均未扩展为多选):多选态无法单值呈现时归「全部」。 +function currentStatusForAdv(){ + if(state.ret) return state.ret; + if(state.statusSet.size===1) return [...state.statusSet][0]; + return '全部'; +} function openAdvSearch(){ - adv={party:state.party,series:state.series,spec:state.spec,category:state.category,op:state.op,reviewer:state.reviewer,status:state.status,dateFrom:state.dateFrom||'',dateTo:state.dateTo||''}; + adv={party:state.party,series:state.series,spec:state.spec,category:state.category,op:state.op,reviewer:state.reviewer,status:currentStatusForAdv(),dateFrom:state.dateFrom||'',dateTo:state.dateTo||''}; advSet('advParty',adv.party); advSet('advSeries',adv.series); advSet('advSpec',adv.spec); advSet('advCategory',adv.category); advSet('advOp',adv.op); advSet('advReviewer',adv.reviewer); advSet('advStatus',adv.status); document.getElementById('advNo').value=state.orderNo||''; @@ -290,7 +338,10 @@ function applyAdv(){ state.productInfo=document.getElementById('advProduct').value.trim(); state.batch=document.getElementById('advBatch').value.trim(); state.party=adv.party; state.series=adv.series; state.spec=adv.spec; state.category=adv.category; - state.op=adv.op; state.reviewer=adv.reviewer; state.status=adv.status; + state.op=adv.op; state.reviewer=adv.reviewer; + if(adv.status==='全部'){ state.statusSet=new Set(); state.ret=''; } + else if(RET_STATUS.includes(adv.status)){ state.ret=adv.status; state.statusSet=new Set(); } + else { state.statusSet=new Set([adv.status]); state.ret=''; } state.dateFrom=adv.dateFrom; state.dateTo=adv.dateTo; state.dateLabel=adv.dateFrom?adv.dateFrom+' ~ '+adv.dateTo:''; state.datePreset=adv.dateFrom?'custom':'all'; state.page=1; closeOverlay('ovAdv'); render(); @@ -298,7 +349,7 @@ function applyAdv(){ function resetAdv(){ adv={party:'全部',series:'全部',spec:'全部',category:'全部',op:'全部',reviewer:'全部',status:'全部',dateFrom:'',dateTo:''}; ['advNo','advProduct','advBatch','advParty','advSeries','advSpec','advCategory','advOp','advReviewer','advStatus','advDate'].forEach(id=>{const el=document.getElementById(id); if(el)el.value='';}); - state.party='全部';state.status='全部';state.orderNo='';state.productInfo='';state.batch='';state.series='全部';state.spec='全部';state.category='全部';state.op='全部';state.reviewer='全部'; + state.party='全部';state.statusSet=new Set();state.ret='';state.orderNo='';state.productInfo='';state.batch='';state.series='全部';state.spec='全部';state.category='全部';state.op='全部';state.reviewer='全部'; applyDatePreset('all'); state.page=1; render(); } function auditBtns(o,idx){ diff --git a/design/prototype/screens/stock-out-list.html b/design/prototype/screens/stock-out-list.html index c56365a..fa3f195 100644 --- a/design/prototype/screens/stock-out-list.html +++ b/design/prototype/screens/stock-out-list.html @@ -24,6 +24,11 @@ .toolbar .daterange svg{width:14px; height:14px; stroke-width:1.8; color:var(--faint);} .toolbar .daterange .dv{color:var(--text);} .toolbar .sp{margin-left:auto;} + /* chip 尾部图标切换:未筛选=下拉箭头,筛选中=× 单清(镜像 inventory.html 多选 chip,待提升至 atoms.css) */ + .chip .cx{display:none;} + .chip.on .cx{display:block; cursor:pointer; color:var(--muted);} + .chip.on .cx:hover{color:var(--text);} + .chip.on .caret{display:none;} /* ---- 状态徽章(新增变体,待提升至 atoms.css) ---- */ .badge.b-草稿{background:var(--info-soft); color:var(--muted);} @@ -64,7 +69,7 @@
客户
仓库
-
状态
+
状态
出库时间
详细搜索
@@ -143,9 +148,14 @@ const ORDERS=[ {no:'CK-20260614-002',date:'2026-06-14',party:'国际会展中心',wh:'主仓',count:1,amount:8400,status:'草稿',op:'张主管',auditor:'',lines:[{name:'西凤酒 旗舰版',qty:24,price:350}]}, {no:'CK-20260613-005',date:'2026-06-13',party:'凯悦酒店宴会',wh:'名酒仓',count:1,amount:32400,status:'已审核',op:'王经理',auditor:'张主管',lines:[{name:'茅台 飞天 53°',qty:12,price:2700}]}, ]; -const STATUS=['全部','草稿','待审核','已审核','已拒绝','部分退单','已退单']; +// 状态筛选 2026-07-14 改多选:主状态(草稿/待审核/已审核/已拒绝)可多选,退单伪状态(部分退单/已退单) +// 与主状态互斥(选其一清另一侧)。默认「草稿+待审核」(D3 用户拍板)。 +const MAIN_STATUS=['草稿','待审核','已审核','已拒绝']; +const RET_STATUS=['部分退单','已退单']; +const DEFAULT_STATUS=['草稿','待审核']; +const STATUS=['全部',...MAIN_STATUS,...RET_STATUS]; // 供详细搜索单选字段沿用 const COLS=[{key:'no',label:'单号'},{key:'party',label:'客户'},{key:'wh',label:'仓库'},{key:'amount',label:'合计金额',num:true},{key:'status',label:'状态'},{key:'op',label:'出库员'},{key:'auditor',label:'审核员'},{key:'act',label:'操作',fixed:true}]; -const state={q:'',party:'全部',wh:'全部',status:'全部',dateFrom:'',dateTo:'',page:1,perPage:10,orderNo:'',productInfo:'',batch:'',series:'全部',spec:'全部',category:'全部',op:'全部',reviewer:'全部'}; +const state={q:'',party:'全部',wh:'全部',statusSet:new Set(DEFAULT_STATUS),ret:'',dateFrom:'',dateTo:'',page:1,perPage:10,orderNo:'',productInfo:'',batch:'',series:'全部',spec:'全部',category:'全部',op:'全部',reviewer:'全部'}; const PARTIES=['全部',...Array.from(new Set(ORDERS.map(o=>o.party)))]; const WAREHOUSES=['全部',...Array.from(new Set(ORDERS.map(o=>o.wh)))]; @@ -158,7 +168,8 @@ const STICON={'草稿':'i-ic06','待审核':'i-ic04','已审核':'i-check','已 function stBadge(v){ return `${v}`; } function numf(n){ return (Number(n)||0).toLocaleString(); } /* 明细行内数字:去¥,缩一号(2026-07-03) */ function filtered(){ return ORDERS.filter(o=>{ - if(state.status!=='全部'&&o.status!==state.status&&o.ret!==state.status)return false; + if(state.ret){ if(o.ret!==state.ret)return false; } + else if(state.statusSet.size&&!state.statusSet.has(o.status))return false; if(state.party!=='全部'&&o.party!==state.party)return false; if(state.wh!=='全部'&&o.wh!==state.wh)return false; if(state.dateFrom&&o.date{ if(state.category&&state.category!=='全部'&&!L.some(l=>(PINFO[l.name]||{}).cat===state.category))return false; if(state.batch){const b=state.batch.toLowerCase(); if(!L.some(l=>{const pi=PINFO[l.name]||{}; return (pi.batch||'').toLowerCase().includes(b)||(pi.code||'').toLowerCase().includes(b);}))return false;} return true; }); } +// 状态 chip 摘要(镜像 inventory.html 多选 chip):无=空、1 项=值本身、多项=「N 项」;退单态优先展示。 +function statusChipLabel(){ + if(state.ret) return state.ret; + if(!state.statusSet.size) return ''; + if(state.statusSet.size===1) return [...state.statusSet][0]; + return state.statusSet.size+' 项'; +} +// 状态是否为默认态(草稿+待审核、无退单)——仅用于「重置」按钮高亮判断,避免默认态常年高亮。 +function isDefaultStatus(){ return !state.ret && state.statusSet.size===2 && state.statusSet.has('草稿') && state.statusSet.has('待审核'); } function syncChips(){ const set=(id,chipId,val)=>{ document.getElementById(id).textContent=val||''; document.getElementById(chipId).classList.toggle('on',!!val); }; set('partyVal','chipParty',state.party==='全部'?'':state.party); set('whVal','chipWh',state.wh==='全部'?'':state.wh); - set('statusVal','chipStatus',state.status==='全部'?'':state.status); + set('statusVal','chipStatus',statusChipLabel()); set('dateVal','chipDate',state.dateLabel||''); const advCount=[state.orderNo,state.productInfo,state.batch, (state.series&&state.series!=='全部')?1:0, @@ -191,13 +211,13 @@ function syncChips(){ advBtnEl.classList.toggle('primary',advCount>0); advBtnEl.classList.toggle('soft',advCount===0); document.getElementById('advCount').textContent=advCount?' · '+advCount:''; - const any=state.q||state.party!=='全部'||state.wh!=='全部'||state.status!=='全部'||state.dateFrom||state.dateTo||advCount>0; + const any=state.q||state.party!=='全部'||state.wh!=='全部'||!isDefaultStatus()||state.dateFrom||state.dateTo||advCount>0; document.getElementById('resetBtn').classList.toggle('primary',!!any); } function render(){ syncChips(); const cols=COLS; - document.getElementById('thead').innerHTML=''+cols.map(c=>{ const cls=(c.num?'num ':'')+(c.key==='act'?'act ':''); if(c.filter){const on=state.status!=='全部'; return `${c.label} `;} return `${c.label}`; }).join('')+''; + document.getElementById('thead').innerHTML=''+cols.map(c=>{ const cls=(c.num?'num ':'')+(c.key==='act'?'act ':''); if(c.filter){const on=!isDefaultStatus(); return `${c.label} `;} return `${c.label}`; }).join('')+''; const rows=filtered(); const start=(state.page-1)*state.perPage; const pageRows=rows.slice(start,start+state.perPage); const tb=document.getElementById('tbody'); if(rows.length===0){ tb.innerHTML=`
没有匹配的出库单 · 试试调整筛选或搜索
`; } else { tb.innerHTML=pageRows.map(o=>{ const idx=ORDERS.indexOf(o); return ''+cols.map(c=>{ if(c.key==='no')return `${o.no}`; if(c.key==='party')return `${o.party}`; if(c.key==='wh')return `${o.wh}`; if(c.key==='amount')return `${money(o.amount)}`; if(c.key==='status')return `${stBadge(o.status)}${o.ret?' '+stBadge(o.ret):''}`; if(c.key==='op')return `${o.op}`; if(c.key==='auditor')return `${o.auditor?o.auditor:''}`; if(c.key==='act')return `
`; return `${o[c.key]}`; }).join('')+''; }).join(''); } @@ -209,10 +229,32 @@ function go(p){ const pages=Math.max(1,Math.ceil(filtered().length/state.perPage const PAGE_SIZES=[10,20,50,100]; function openPageSize(e){ openMenu(e.currentTarget, PAGE_SIZES.map(n=>({v:String(n),label:String(n),sel:n===state.perPage})), v=>{ closeMenus(); setPerPage(v); }, 'pgsize-menu', 88); } function setPerPage(v){ state.perPage=+v; state.page=1; const el=document.getElementById('pageSizeVal'); if(el)el.textContent=v; render(); } -function filterStatus(s){ state.status=s; state.page=1; render(); } -function clearFilters(){ state.q='';state.party='全部';state.wh='全部';state.status='全部';state.dateFrom='';state.dateTo='';state.dateLabel='';state.datePreset='all';state.orderNo='';state.productInfo='';state.batch='';state.series='全部';state.spec='全部';state.category='全部';state.op='全部';state.reviewer='全部';state.page=1; document.getElementById('searchInput').value=''; render(); } +// KPI 卡点击 = 覆盖为该单值(D5,非 toggle 集合成员)。 +function filterStatus(s){ + if(RET_STATUS.includes(s)){ state.ret=s; state.statusSet=new Set(); } + else { state.ret=''; state.statusSet=new Set([s]); } + state.page=1; render(); +} +// 状态 chip 的 × 单清:回到默认「草稿+待审核」,不是回「全部」(D3)。 +function clearStatus(e){ e.stopPropagation(); state.statusSet=new Set(DEFAULT_STATUS); state.ret=''; state.page=1; render(); } +function clearFilters(){ state.q='';state.party='全部';state.wh='全部';state.statusSet=new Set(DEFAULT_STATUS);state.ret='';state.dateFrom='';state.dateTo='';state.dateLabel='';state.datePreset='all';state.orderNo='';state.productInfo='';state.batch='';state.series='全部';state.spec='全部';state.category='全部';state.op='全部';state.reviewer='全部';state.page=1; document.getElementById('searchInput').value=''; render(); } -function openStatusFilter(t){ openMenu(t,STATUS.map(v=>({v,label:v,sel:v===state.status})),v=>{ state.status=v; state.page=1; closeMenus(); render(); }); } +// 状态多选菜单:主状态 checkbox 多选 + 退单伪状态互斥(选其一清主状态多选,反之亦然); +// 菜单勾选后不关闭(镜像 inventory.html openDimFilter 的重开手法)。 +function openStatusFilter(t){ + const opts=[...MAIN_STATUS.map(v=>({v,label:v,sel:state.statusSet.has(v)})), + ...RET_STATUS.map(v=>({v,label:v,sel:state.ret===v}))]; + openMenu(t,opts,v=>{ + if(RET_STATUS.includes(v)){ + state.ret = state.ret===v ? '' : v; + state.statusSet=new Set(); + } else { + state.ret=''; + state.statusSet.has(v)?state.statusSet.delete(v):state.statusSet.add(v); + } + state.page=1; render(); openStatusFilter(t); + }); +} function openPartyFilter(t){ openSearchMenu(t,PARTIES.map(v=>({v,label:v,sel:v===state.party})),v=>{ state.party=v; state.page=1; closeMenus(); render(); },{placeholder:'搜索客户…'}); } function openWhFilter(t){ openMenu(t,WAREHOUSES.map(v=>({v,label:v,sel:v===state.wh})),v=>{ state.wh=v; state.page=1; closeMenus(); render(); }); } const TODAY='2026-06-22'; @@ -259,8 +301,14 @@ const OP_OPTS=['全部',...Array.from(new Set(ORDERS.map(o=>o.op)))]; const REVIEWER_OPTS=['全部',...Array.from(new Set(ORDERS.map(o=>o.auditor).filter(Boolean)))]; let adv={}; function advSet(id,val){ const el=document.getElementById(id); if(el)el.value=(!val||val==='全部')?'':val; } +// 详细搜索的「状态」字段仍是单选(原型/Flutter 均未扩展为多选):多选态无法单值呈现时归「全部」。 +function currentStatusForAdv(){ + if(state.ret) return state.ret; + if(state.statusSet.size===1) return [...state.statusSet][0]; + return '全部'; +} function openAdvSearch(){ - adv={party:state.party,series:state.series,spec:state.spec,category:state.category,op:state.op,reviewer:state.reviewer,status:state.status,dateFrom:state.dateFrom||'',dateTo:state.dateTo||''}; + adv={party:state.party,series:state.series,spec:state.spec,category:state.category,op:state.op,reviewer:state.reviewer,status:currentStatusForAdv(),dateFrom:state.dateFrom||'',dateTo:state.dateTo||''}; advSet('advParty',adv.party); advSet('advSeries',adv.series); advSet('advSpec',adv.spec); advSet('advCategory',adv.category); advSet('advOp',adv.op); advSet('advReviewer',adv.reviewer); advSet('advStatus',adv.status); document.getElementById('advNo').value=state.orderNo||''; @@ -285,7 +333,10 @@ function applyAdv(){ state.productInfo=document.getElementById('advProduct').value.trim(); state.batch=document.getElementById('advBatch').value.trim(); state.party=adv.party; state.series=adv.series; state.spec=adv.spec; state.category=adv.category; - state.op=adv.op; state.reviewer=adv.reviewer; state.status=adv.status; + state.op=adv.op; state.reviewer=adv.reviewer; + if(adv.status==='全部'){ state.statusSet=new Set(); state.ret=''; } + else if(RET_STATUS.includes(adv.status)){ state.ret=adv.status; state.statusSet=new Set(); } + else { state.statusSet=new Set([adv.status]); state.ret=''; } state.dateFrom=adv.dateFrom; state.dateTo=adv.dateTo; state.dateLabel=adv.dateFrom?adv.dateFrom+' ~ '+adv.dateTo:''; state.datePreset=adv.dateFrom?'custom':'all'; state.page=1; closeOverlay('ovAdv'); render(); @@ -293,7 +344,7 @@ function applyAdv(){ function resetAdv(){ adv={party:'全部',series:'全部',spec:'全部',category:'全部',op:'全部',reviewer:'全部',status:'全部',dateFrom:'',dateTo:''}; ['advNo','advProduct','advBatch','advParty','advSeries','advSpec','advCategory','advOp','advReviewer','advStatus','advDate'].forEach(id=>{const el=document.getElementById(id); if(el)el.value='';}); - state.party='全部';state.status='全部';state.orderNo='';state.productInfo='';state.batch='';state.series='全部';state.spec='全部';state.category='全部';state.op='全部';state.reviewer='全部'; + state.party='全部';state.statusSet=new Set();state.ret='';state.orderNo='';state.productInfo='';state.batch='';state.series='全部';state.spec='全部';state.category='全部';state.op='全部';state.reviewer='全部'; applyDatePreset('all'); state.page=1; render(); } function auditBtns(o,idx){