diff --git a/CHANGELOG-client.md b/CHANGELOG-client.md index 7f6937a..30c9410 100644 --- a/CHANGELOG-client.md +++ b/CHANGELOG-client.md @@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.0.62] - 2026-06-20 + +### 改进 +- 入库单、出库单的仓库、供应商、客户下拉框改为可搜索:点击后弹出搜索框,支持按名称、拼音首字母或编码快速筛选,选项多时无需再逐条翻找 + +### 修复 +- 修复账号在别处登录或被管理员强制下线后、退回登录页却没有任何提示的问题,现在会弹窗明确告知「登录已失效」 + ## [1.0.61] - 2026-06-20 ### 改进 diff --git a/client/lib/screens/auth/login_screen.dart b/client/lib/screens/auth/login_screen.dart index adcc676..444670d 100644 --- a/client/lib/screens/auth/login_screen.dart +++ b/client/lib/screens/auth/login_screen.dart @@ -94,6 +94,34 @@ class _LoginScreenState extends ConsumerState { Timer(AppConstants.dropdownCloseDelay, _closeUsername); } }); + // 处理「进入登录页之前」就已置入的会话失效提示:登出在 login_screen 挂载前就 + // 设好了 sessionEndedMessageProvider,而 build 里的 ref.listen 只捕获注册之后的 + // 变化,会漏掉这个既有值 → 之前「被顶下线无提示」的根因。这里在首帧主动读一次。 + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted) return; + final msg = ref.read(sessionEndedMessageProvider); + if (msg != null && msg.isNotEmpty) _showSessionEndedDialog(msg); + }); + } + + /// 弹出「登录已失效 / 被强制下线」提示弹窗,并清空 provider 避免重复弹。 + void _showSessionEndedDialog(String msg) { + if (!mounted) return; + ref.read(sessionEndedMessageProvider.notifier).state = null; + showDialog( + context: context, + builder: (ctx) => AlertDialog( + icon: const Icon(Icons.lock_outline, color: AppTheme.danger), + title: const Text('登录已失效'), + content: Text(msg), + actions: [ + TextButton( + onPressed: () => Navigator.of(ctx).pop(), + child: const Text('我知道了'), + ), + ], + ), + ); } Future _loadHistory() async { @@ -346,14 +374,12 @@ class _LoginScreenState extends ConsumerState { @override Widget build(BuildContext context) { - // 被踢下线 / 会话失效:登出后跳回登录页,弹一次提示并清空 + // 已在登录页时会话再失效(例如停留在登录页期间后台请求被吊销):弹窗提示。 + // 进入登录页之前就置入的既有值由 initState 的首帧读取处理。 ref.listen(sessionEndedMessageProvider, (prev, next) { if (next != null && next.isNotEmpty) { WidgetsBinding.instance.addPostFrameCallback((_) { - if (!mounted) return; - ScaffoldMessenger.of(context).showSnackBar(SnackBar( - content: Text(next), backgroundColor: AppTheme.danger)); - ref.read(sessionEndedMessageProvider.notifier).state = null; + _showSessionEndedDialog(next); }); } }); diff --git a/client/lib/screens/stock_in/stock_in_form_screen.dart b/client/lib/screens/stock_in/stock_in_form_screen.dart index ab8f2ec..8dbfeb7 100644 --- a/client/lib/screens/stock_in/stock_in_form_screen.dart +++ b/client/lib/screens/stock_in/stock_in_form_screen.dart @@ -604,24 +604,19 @@ class _StockInFormScreenState extends ConsumerState { const LinearProgressIndicator(), error: (_, __) => const Text('加载失败'), data: (warehouses) => - DropdownButtonFormField( - value: _warehouseId, - hint: const Text('请选择仓库', - style: TextStyle(fontSize: 13)), - items: warehouses - .map((w) => DropdownMenuItem( - value: w.id, - child: Text(w.name, - style: const TextStyle( - fontSize: 13)))) + SearchableOptionField( + options: warehouses + .map((w) => OptionItem( + id: w.id, name: w.name)) .toList(), + selectedId: _warehouseId, + hint: '请选择仓库', + dialogTitle: '选择入库仓库', + isRequired: true, onChanged: (v) { setState(() => _warehouseId = v); if (v != null) _loadInventory(v); }, - validator: (v) => - v == null ? '不能为空' : null, - decoration: const InputDecoration(), ), ), ), @@ -633,22 +628,19 @@ class _StockInFormScreenState extends ConsumerState { const LinearProgressIndicator(), error: (_, __) => const Text('加载失败'), data: (result) => - DropdownButtonFormField( - value: _partnerId, - hint: const Text('请选择供应商', - style: TextStyle(fontSize: 13)), - items: result.data - .map((p) => DropdownMenuItem( - value: p.id, - child: Text(p.name, - style: const TextStyle( - fontSize: 13)))) + SearchableOptionField( + options: result.data + .map((p) => OptionItem( + id: p.id, + name: p.name, + code: p.code)) .toList(), + selectedId: _partnerId, + hint: '请选择供应商', + dialogTitle: '选择供应商', + isRequired: true, onChanged: (v) => setState(() => _partnerId = v), - validator: (v) => - v == null ? '不能为空' : null, - decoration: const InputDecoration(), ), ), ), diff --git a/client/lib/screens/stock_out/stock_out_form_screen.dart b/client/lib/screens/stock_out/stock_out_form_screen.dart index 12ede4f..e59d4b8 100644 --- a/client/lib/screens/stock_out/stock_out_form_screen.dart +++ b/client/lib/screens/stock_out/stock_out_form_screen.dart @@ -9,6 +9,7 @@ import '../../core/utils/print_util.dart'; import '../../core/utils/date_util.dart'; import '../../models/stock_out.dart'; import '../../widgets/date_picker_field.dart'; +import '../../widgets/searchable_option_field.dart'; import '../../core/config/app_constants.dart'; import '../../providers/inventory_provider.dart'; import '../../providers/partner_provider.dart'; @@ -498,24 +499,19 @@ class _StockOutFormScreenState extends ConsumerState { const LinearProgressIndicator(), error: (_, __) => const Text('加载失败'), data: (warehouses) => - DropdownButtonFormField( - value: _warehouseId, - hint: const Text('请选择仓库', - style: TextStyle(fontSize: 13)), - items: warehouses - .map((w) => DropdownMenuItem( - value: w.id, - child: Text(w.name, - style: const TextStyle( - fontSize: 13)))) + SearchableOptionField( + options: warehouses + .map((w) => OptionItem( + id: w.id, name: w.name)) .toList(), + selectedId: _warehouseId, + hint: '请选择仓库', + dialogTitle: '选择出库仓库', + isRequired: true, onChanged: (v) { setState(() => _warehouseId = v); if (v != null) _loadInventory(v); }, - validator: (v) => - v == null ? '不能为空' : null, - decoration: const InputDecoration(), ), ), ), @@ -526,20 +522,18 @@ class _StockOutFormScreenState extends ConsumerState { const LinearProgressIndicator(), error: (_, __) => const Text('加载失败'), data: (result) => - DropdownButtonFormField( - value: _partnerId, - hint: const Text('请选择客户', - style: TextStyle(fontSize: 13)), - items: result.data - .map((p) => DropdownMenuItem( - value: p.id, - child: Text(p.name, - style: const TextStyle( - fontSize: 13)))) + SearchableOptionField( + options: result.data + .map((p) => OptionItem( + id: p.id, + name: p.name, + code: p.code)) .toList(), + selectedId: _partnerId, + hint: '请选择客户', + dialogTitle: '选择客户', onChanged: (v) => setState(() => _partnerId = v), - decoration: const InputDecoration(), ), ), ),