Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 648d9bc0d5 |
@@ -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/),
|
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).
|
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
|
## [1.0.61] - 2026-06-20
|
||||||
|
|
||||||
### 改进
|
### 改进
|
||||||
|
|||||||
@@ -94,6 +94,34 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
|
|||||||
Timer(AppConstants.dropdownCloseDelay, _closeUsername);
|
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<void>(
|
||||||
|
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<void> _loadHistory() async {
|
Future<void> _loadHistory() async {
|
||||||
@@ -346,14 +374,12 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
// 被踢下线 / 会话失效:登出后跳回登录页,弹一次提示并清空
|
// 已在登录页时会话再失效(例如停留在登录页期间后台请求被吊销):弹窗提示。
|
||||||
|
// 进入登录页之前就置入的既有值由 initState 的首帧读取处理。
|
||||||
ref.listen<String?>(sessionEndedMessageProvider, (prev, next) {
|
ref.listen<String?>(sessionEndedMessageProvider, (prev, next) {
|
||||||
if (next != null && next.isNotEmpty) {
|
if (next != null && next.isNotEmpty) {
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
if (!mounted) return;
|
_showSessionEndedDialog(next);
|
||||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
|
||||||
content: Text(next), backgroundColor: AppTheme.danger));
|
|
||||||
ref.read(sessionEndedMessageProvider.notifier).state = null;
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -604,24 +604,19 @@ class _StockInFormScreenState extends ConsumerState<StockInFormScreen> {
|
|||||||
const LinearProgressIndicator(),
|
const LinearProgressIndicator(),
|
||||||
error: (_, __) => const Text('加载失败'),
|
error: (_, __) => const Text('加载失败'),
|
||||||
data: (warehouses) =>
|
data: (warehouses) =>
|
||||||
DropdownButtonFormField<int>(
|
SearchableOptionField(
|
||||||
value: _warehouseId,
|
options: warehouses
|
||||||
hint: const Text('请选择仓库',
|
.map((w) => OptionItem(
|
||||||
style: TextStyle(fontSize: 13)),
|
id: w.id, name: w.name))
|
||||||
items: warehouses
|
|
||||||
.map((w) => DropdownMenuItem(
|
|
||||||
value: w.id,
|
|
||||||
child: Text(w.name,
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 13))))
|
|
||||||
.toList(),
|
.toList(),
|
||||||
|
selectedId: _warehouseId,
|
||||||
|
hint: '请选择仓库',
|
||||||
|
dialogTitle: '选择入库仓库',
|
||||||
|
isRequired: true,
|
||||||
onChanged: (v) {
|
onChanged: (v) {
|
||||||
setState(() => _warehouseId = v);
|
setState(() => _warehouseId = v);
|
||||||
if (v != null) _loadInventory(v);
|
if (v != null) _loadInventory(v);
|
||||||
},
|
},
|
||||||
validator: (v) =>
|
|
||||||
v == null ? '不能为空' : null,
|
|
||||||
decoration: const InputDecoration(),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -633,22 +628,19 @@ class _StockInFormScreenState extends ConsumerState<StockInFormScreen> {
|
|||||||
const LinearProgressIndicator(),
|
const LinearProgressIndicator(),
|
||||||
error: (_, __) => const Text('加载失败'),
|
error: (_, __) => const Text('加载失败'),
|
||||||
data: (result) =>
|
data: (result) =>
|
||||||
DropdownButtonFormField<int>(
|
SearchableOptionField(
|
||||||
value: _partnerId,
|
options: result.data
|
||||||
hint: const Text('请选择供应商',
|
.map((p) => OptionItem(
|
||||||
style: TextStyle(fontSize: 13)),
|
id: p.id,
|
||||||
items: result.data
|
name: p.name,
|
||||||
.map((p) => DropdownMenuItem(
|
code: p.code))
|
||||||
value: p.id,
|
|
||||||
child: Text(p.name,
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 13))))
|
|
||||||
.toList(),
|
.toList(),
|
||||||
|
selectedId: _partnerId,
|
||||||
|
hint: '请选择供应商',
|
||||||
|
dialogTitle: '选择供应商',
|
||||||
|
isRequired: true,
|
||||||
onChanged: (v) =>
|
onChanged: (v) =>
|
||||||
setState(() => _partnerId = v),
|
setState(() => _partnerId = v),
|
||||||
validator: (v) =>
|
|
||||||
v == null ? '不能为空' : null,
|
|
||||||
decoration: const InputDecoration(),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import '../../core/utils/print_util.dart';
|
|||||||
import '../../core/utils/date_util.dart';
|
import '../../core/utils/date_util.dart';
|
||||||
import '../../models/stock_out.dart';
|
import '../../models/stock_out.dart';
|
||||||
import '../../widgets/date_picker_field.dart';
|
import '../../widgets/date_picker_field.dart';
|
||||||
|
import '../../widgets/searchable_option_field.dart';
|
||||||
import '../../core/config/app_constants.dart';
|
import '../../core/config/app_constants.dart';
|
||||||
import '../../providers/inventory_provider.dart';
|
import '../../providers/inventory_provider.dart';
|
||||||
import '../../providers/partner_provider.dart';
|
import '../../providers/partner_provider.dart';
|
||||||
@@ -498,24 +499,19 @@ class _StockOutFormScreenState extends ConsumerState<StockOutFormScreen> {
|
|||||||
const LinearProgressIndicator(),
|
const LinearProgressIndicator(),
|
||||||
error: (_, __) => const Text('加载失败'),
|
error: (_, __) => const Text('加载失败'),
|
||||||
data: (warehouses) =>
|
data: (warehouses) =>
|
||||||
DropdownButtonFormField<int>(
|
SearchableOptionField(
|
||||||
value: _warehouseId,
|
options: warehouses
|
||||||
hint: const Text('请选择仓库',
|
.map((w) => OptionItem(
|
||||||
style: TextStyle(fontSize: 13)),
|
id: w.id, name: w.name))
|
||||||
items: warehouses
|
|
||||||
.map((w) => DropdownMenuItem(
|
|
||||||
value: w.id,
|
|
||||||
child: Text(w.name,
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 13))))
|
|
||||||
.toList(),
|
.toList(),
|
||||||
|
selectedId: _warehouseId,
|
||||||
|
hint: '请选择仓库',
|
||||||
|
dialogTitle: '选择出库仓库',
|
||||||
|
isRequired: true,
|
||||||
onChanged: (v) {
|
onChanged: (v) {
|
||||||
setState(() => _warehouseId = v);
|
setState(() => _warehouseId = v);
|
||||||
if (v != null) _loadInventory(v);
|
if (v != null) _loadInventory(v);
|
||||||
},
|
},
|
||||||
validator: (v) =>
|
|
||||||
v == null ? '不能为空' : null,
|
|
||||||
decoration: const InputDecoration(),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -526,20 +522,18 @@ class _StockOutFormScreenState extends ConsumerState<StockOutFormScreen> {
|
|||||||
const LinearProgressIndicator(),
|
const LinearProgressIndicator(),
|
||||||
error: (_, __) => const Text('加载失败'),
|
error: (_, __) => const Text('加载失败'),
|
||||||
data: (result) =>
|
data: (result) =>
|
||||||
DropdownButtonFormField<int>(
|
SearchableOptionField(
|
||||||
value: _partnerId,
|
options: result.data
|
||||||
hint: const Text('请选择客户',
|
.map((p) => OptionItem(
|
||||||
style: TextStyle(fontSize: 13)),
|
id: p.id,
|
||||||
items: result.data
|
name: p.name,
|
||||||
.map((p) => DropdownMenuItem(
|
code: p.code))
|
||||||
value: p.id,
|
|
||||||
child: Text(p.name,
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 13))))
|
|
||||||
.toList(),
|
.toList(),
|
||||||
|
selectedId: _partnerId,
|
||||||
|
hint: '请选择客户',
|
||||||
|
dialogTitle: '选择客户',
|
||||||
onChanged: (v) =>
|
onChanged: (v) =>
|
||||||
setState(() => _partnerId = v),
|
setState(() => _partnerId = v),
|
||||||
decoration: const InputDecoration(),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
Reference in New Issue
Block a user