feat(client): 实时授权状态与会话失效处理
- 心跳读取 /auth/ping 回带的授权概况,直接刷新横幅/状态栏/只读门禁,省去单独轮询 /license/info - 账号被停用/删除(401 USER_DISABLED)即强制重新登录 - 令牌持久化经串行队列 + 会话代号守卫,杜绝续期写入与登出交叉把失效 token 写回 - 写操作门禁(write_guard)+ 授权文案(license_copy)按 grace/readonly/locked 分阶段降级 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -79,6 +79,33 @@ class _InventoryListScreenState extends ConsumerState<InventoryListScreen> {
|
||||
ref.read(inventoryListProvider.notifier).setKeyword(_searchCtrl.text.trim());
|
||||
}
|
||||
|
||||
/// 备注列展示:editable 时附带编辑图标(用于 WriteGuard 的可点子控件),
|
||||
/// 否则纯文本(只读角色占位)。
|
||||
Widget _remarkDisplay(Inventory item, {required bool editable}) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
item.remark.isEmpty
|
||||
? '—'
|
||||
: item.remark.length > 4
|
||||
? '${item.remark.substring(0, 4)}…'
|
||||
: item.remark,
|
||||
style: TextStyle(
|
||||
color: item.remark.isEmpty
|
||||
? AppTheme.textSecondary
|
||||
: AppTheme.textPrimary,
|
||||
),
|
||||
),
|
||||
if (editable) ...[
|
||||
const SizedBox(width: 4),
|
||||
const Icon(Icons.edit_outlined,
|
||||
size: 12, color: AppTheme.textSecondary),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _editRemark(BuildContext context, Inventory item) async {
|
||||
final ctrl = TextEditingController(text: item.remark);
|
||||
final saved = await showDialog<String>(
|
||||
@@ -304,28 +331,13 @@ class _InventoryListScreenState extends ConsumerState<InventoryListScreen> {
|
||||
'remark' => DataCell(Tooltip(
|
||||
message: item.remark.isEmpty ? '' : item.remark,
|
||||
waitDuration: const Duration(milliseconds: 300),
|
||||
child: GestureDetector(
|
||||
onTap: WriteGuard.isReadonly(ref)
|
||||
? null
|
||||
: () => _editRemark(context, item),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
item.remark.isEmpty
|
||||
? '—'
|
||||
: item.remark.length > 4
|
||||
? '${item.remark.substring(0, 4)}…'
|
||||
: item.remark,
|
||||
style: TextStyle(
|
||||
color: item.remark.isEmpty
|
||||
? AppTheme.textSecondary
|
||||
: AppTheme.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
const Icon(Icons.edit_outlined, size: 12, color: AppTheme.textSecondary),
|
||||
],
|
||||
// 内联编辑入口统一交给 WriteGuard:只读角色显示纯文本(无编辑图标),
|
||||
// 授权过期则由 WriteGuard 自动置灰并在点击时弹提示——不再手搓三元 + toast。
|
||||
child: WriteGuard(
|
||||
placeholder: _remarkDisplay(item, editable: false),
|
||||
child: GestureDetector(
|
||||
onTap: () => _editRemark(context, item),
|
||||
child: _remarkDisplay(item, editable: true),
|
||||
),
|
||||
))),
|
||||
'status' => DataCell(_InventoryStatusBadge(item)),
|
||||
@@ -374,9 +386,11 @@ class _InventoryListScreenState extends ConsumerState<InventoryListScreen> {
|
||||
],
|
||||
actions: [
|
||||
if (!WriteGuard.isReadonly(ref))
|
||||
TextButton(
|
||||
onPressed: () => _editRemark(context, item),
|
||||
child: const Text('备注', style: TextStyle(fontSize: 13)),
|
||||
WriteGuard(
|
||||
child: TextButton(
|
||||
onPressed: () => _editRemark(context, item),
|
||||
child: const Text('备注', style: TextStyle(fontSize: 13)),
|
||||
),
|
||||
),
|
||||
if (item.productId != null)
|
||||
TextButton(
|
||||
@@ -622,16 +636,19 @@ class _InventoryListScreenState extends ConsumerState<InventoryListScreen> {
|
||||
Row(
|
||||
children: [
|
||||
if (canCheck)
|
||||
OutlinedButton.icon(
|
||||
onPressed: () => context.go('/inventory/check'),
|
||||
icon: const Icon(Icons.fact_check, size: 16),
|
||||
label: const Text('盘点'),
|
||||
style: OutlinedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 10, vertical: 6),
|
||||
minimumSize: Size.zero,
|
||||
tapTargetSize:
|
||||
MaterialTapTargetSize.shrinkWrap,
|
||||
WriteGuard(
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: () =>
|
||||
context.go('/inventory/check'),
|
||||
icon: const Icon(Icons.fact_check, size: 16),
|
||||
label: const Text('盘点'),
|
||||
style: OutlinedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 10, vertical: 6),
|
||||
minimumSize: Size.zero,
|
||||
tapTargetSize:
|
||||
MaterialTapTargetSize.shrinkWrap,
|
||||
),
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
@@ -671,10 +688,12 @@ class _InventoryListScreenState extends ConsumerState<InventoryListScreen> {
|
||||
SizedBox(width: 220, child: searchField),
|
||||
const SizedBox(width: 12),
|
||||
if (canCheck) ...[
|
||||
OutlinedButton.icon(
|
||||
onPressed: () => context.go('/inventory/check'),
|
||||
icon: const Icon(Icons.fact_check, size: 16),
|
||||
label: const Text('发起盘点'),
|
||||
WriteGuard(
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: () => context.go('/inventory/check'),
|
||||
icon: const Icon(Icons.fact_check, size: 16),
|
||||
label: const Text('发起盘点'),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user