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:
qzl
2026-03-18 13:35:25 +08:00
parent 19981964fb
commit b34697660d
56 changed files with 2602 additions and 784 deletions
@@ -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'),