feat: 实现 Auth 全局状态机与 401 统一处理机制
- 新增 AuthSessionInvalidated 事件处理 token 失效场景 - ApiInterceptor 新增 authFailureCallback 单飞机制 - AuthBloc 区分 manual logout 与 auto expiry 语义 - 新增 startup recovery fallback 防止启动卡死 feat: 重构 Calendar DayWeek 视图事件布局引擎 - 新增 DayEventLayoutEngine 解耦事件计算与渲染 - 新增 DayTimelineMetrics 统一时间轴常量 - 新增 DayViewScale 支持捏合缩放 feat: 新增 Settings 页面共享 UI 组件 - 新增 BackTitlePageHeader 统一页面 header - 新增 DetailHeaderActionMenu 统一操作菜单 - 新增 DestructiveActionSheet 统一删除确认 - 新增 AppToggleSwitch 统一开关组件 feat: Chat UI Schema 支持导航操作 - 支持 navigation 类型 action 触发内部路由跳转 - 新增路径验证与参数处理 chore: 更新相关测试覆盖 auth 失效路径
This commit is contained in:
@@ -4,13 +4,17 @@ import 'package:lucide_icons/lucide_icons.dart';
|
||||
import '../../../../core/di/injection.dart';
|
||||
import '../../../../core/theme/design_tokens.dart';
|
||||
import '../../../../shared/widgets/app_loading_indicator.dart';
|
||||
import '../../../../shared/widgets/page_header.dart' as widgets;
|
||||
import '../../../../shared/widgets/back_title_page_header.dart';
|
||||
import '../../../../shared/widgets/detail_header_action_menu.dart';
|
||||
import '../../../../shared/widgets/destructive_action_sheet.dart';
|
||||
import '../../../../shared/widgets/app_button.dart';
|
||||
import '../../../../shared/widgets/toast/toast.dart';
|
||||
import '../../../../shared/widgets/toast/toast_type.dart';
|
||||
import '../../../calendar/data/calendar_api.dart';
|
||||
import '../../data/todo_api.dart';
|
||||
|
||||
enum _TodoHeaderAction { edit, delete }
|
||||
|
||||
class TodoDetailScreen extends StatefulWidget {
|
||||
final String todoId;
|
||||
|
||||
@@ -87,8 +91,9 @@ class _TodoDetailScreenState extends State<TodoDetailScreen> {
|
||||
backgroundColor: AppColors.todoBg,
|
||||
body: SafeArea(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
_buildHeader(context),
|
||||
_buildHeader(),
|
||||
Expanded(child: _buildContent()),
|
||||
],
|
||||
),
|
||||
@@ -96,39 +101,47 @@ class _TodoDetailScreenState extends State<TodoDetailScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeader(BuildContext context) {
|
||||
return SizedBox(
|
||||
height: 64,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 16, right: 16, top: 12, bottom: 8),
|
||||
child: Row(
|
||||
children: [
|
||||
widgets.BackButton(onPressed: () => Navigator.of(context).pop()),
|
||||
const Spacer(),
|
||||
if (_todo != null) ...[
|
||||
IconButton(
|
||||
onPressed: _editTodo,
|
||||
icon: const Icon(
|
||||
LucideIcons.pencil,
|
||||
size: 20,
|
||||
color: AppColors.slate600,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: _deleteTodo,
|
||||
icon: const Icon(
|
||||
LucideIcons.trash2,
|
||||
size: 20,
|
||||
color: Colors.red,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
Widget _buildHeader() {
|
||||
return BackTitlePageHeader(
|
||||
title: '待办详情',
|
||||
onBack: () => context.pop(),
|
||||
trailing: _buildHeaderMenu(),
|
||||
);
|
||||
}
|
||||
|
||||
Widget? _buildHeaderMenu() {
|
||||
if (_todo == null) {
|
||||
return null;
|
||||
}
|
||||
return DetailHeaderActionMenu<_TodoHeaderAction>(
|
||||
items: const [
|
||||
DetailHeaderActionItem<_TodoHeaderAction>(
|
||||
value: _TodoHeaderAction.edit,
|
||||
label: '编辑',
|
||||
icon: LucideIcons.pencil,
|
||||
),
|
||||
DetailHeaderActionItem<_TodoHeaderAction>(
|
||||
value: _TodoHeaderAction.delete,
|
||||
label: '删除',
|
||||
icon: LucideIcons.trash2,
|
||||
isDestructive: true,
|
||||
),
|
||||
],
|
||||
onSelected: _handleHeaderAction,
|
||||
);
|
||||
}
|
||||
|
||||
void _handleHeaderAction(_TodoHeaderAction action) {
|
||||
switch (action) {
|
||||
case _TodoHeaderAction.edit:
|
||||
_editTodo();
|
||||
return;
|
||||
case _TodoHeaderAction.delete:
|
||||
_deleteTodo();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildContent() {
|
||||
if (_isLoading) {
|
||||
return const Center(child: AppLoadingIndicator(size: 22));
|
||||
@@ -382,22 +395,11 @@ class _TodoDetailScreenState extends State<TodoDetailScreen> {
|
||||
}
|
||||
|
||||
void _deleteTodo() async {
|
||||
final confirm = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('确认删除'),
|
||||
content: const Text('确定要删除这个待办吗?'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(false),
|
||||
child: const Text('取消'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(true),
|
||||
child: const Text('删除', style: TextStyle(color: Colors.red)),
|
||||
),
|
||||
],
|
||||
),
|
||||
final confirm = await showDestructiveActionSheet(
|
||||
context,
|
||||
title: '删除待办',
|
||||
message: '确定要删除这个待办吗?',
|
||||
confirmText: '确认删除',
|
||||
);
|
||||
|
||||
if (confirm == true) {
|
||||
|
||||
@@ -8,6 +8,7 @@ import '../../../../shared/widgets/app_loading_indicator.dart';
|
||||
import '../../../../shared/widgets/app_pull_refresh_feedback.dart';
|
||||
import '../../../../shared/widgets/app_pressable.dart';
|
||||
import '../../../../shared/widgets/app_sheet_input_field.dart';
|
||||
import '../../../../shared/widgets/back_title_page_header.dart';
|
||||
import '../../../../shared/widgets/toast/toast.dart';
|
||||
import '../../../../shared/widgets/toast/toast_type.dart';
|
||||
import '../../../calendar/data/calendar_api.dart';
|
||||
@@ -167,74 +168,57 @@ class _TodoQuadrantsScreenState extends State<TodoQuadrantsScreen> {
|
||||
}
|
||||
|
||||
Widget _buildHeader() {
|
||||
return SizedBox(
|
||||
height: 72,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 16, right: 16, top: 14, bottom: 8),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
const Text(
|
||||
'待办事项',
|
||||
style: TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColors.slate900,
|
||||
return BackTitlePageHeader(
|
||||
title: '待办事项',
|
||||
showBackButton: false,
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
AppPressable(
|
||||
borderRadius: BorderRadius.circular(AppRadius.full),
|
||||
onTap: _loadTodos,
|
||||
child: Container(
|
||||
width: 36,
|
||||
height: 36,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.messageBtnWrap,
|
||||
borderRadius: BorderRadius.circular(AppRadius.full),
|
||||
border: Border.all(color: AppColors.messageBtnBorder),
|
||||
),
|
||||
child: const Icon(
|
||||
LucideIcons.refreshCcw,
|
||||
size: 18,
|
||||
color: AppColors.slate600,
|
||||
),
|
||||
),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
AppPressable(
|
||||
borderRadius: BorderRadius.circular(AppRadius.full),
|
||||
onTap: _loadTodos,
|
||||
child: Container(
|
||||
width: 36,
|
||||
height: 36,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.messageBtnWrap,
|
||||
borderRadius: BorderRadius.circular(AppRadius.full),
|
||||
border: Border.all(color: AppColors.messageBtnBorder),
|
||||
),
|
||||
child: const Icon(
|
||||
LucideIcons.refreshCcw,
|
||||
size: 18,
|
||||
color: AppColors.slate600,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: AppSpacing.sm),
|
||||
AppPressable(
|
||||
borderRadius: BorderRadius.circular(AppRadius.full),
|
||||
onTap: _addTodo,
|
||||
child: Container(
|
||||
width: 36,
|
||||
height: 36,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.blue600,
|
||||
borderRadius: BorderRadius.circular(AppRadius.full),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: AppColors.blue300.withValues(alpha: 0.28),
|
||||
blurRadius: AppRadius.lg,
|
||||
offset: const Offset(0, AppSpacing.xs),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: AppSpacing.sm),
|
||||
AppPressable(
|
||||
borderRadius: BorderRadius.circular(AppRadius.full),
|
||||
onTap: _addTodo,
|
||||
child: Container(
|
||||
width: 36,
|
||||
height: 36,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.blue600,
|
||||
borderRadius: BorderRadius.circular(AppRadius.full),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: AppColors.blue300.withValues(alpha: 0.28),
|
||||
blurRadius: AppRadius.lg,
|
||||
offset: const Offset(0, AppSpacing.xs),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: const Icon(
|
||||
LucideIcons.plus,
|
||||
size: 18,
|
||||
color: AppColors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
child: const Icon(
|
||||
LucideIcons.plus,
|
||||
size: 18,
|
||||
color: AppColors.white,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -395,9 +379,9 @@ class _TodoQuadrantsScreenState extends State<TodoQuadrantsScreen> {
|
||||
final dateStr =
|
||||
'${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}';
|
||||
if (viewType == CalendarViewType.month) {
|
||||
context.push('/calendar/month');
|
||||
context.go('/calendar/month');
|
||||
} else {
|
||||
context.push('/calendar/dayweek?date=$dateStr');
|
||||
context.go('/calendar/dayweek?date=$dateStr');
|
||||
}
|
||||
},
|
||||
onHomeTap: () => context.go('/home'),
|
||||
|
||||
Reference in New Issue
Block a user