chore: v6.6.6 版本迭代更新
主要变更: 1. 重构"国学"相关模块为"经典名句",统一命名规范 2. 重命名"阅读报告"为"使用报告",调整相关文案与配置 3. 修复iOS模拟器图片缓存兼容问题,优化图表渲染逻辑 4. 新增设备活跃状态前端兜底判断,修复在线计数异常 5. 完善登录/注册流程,新增忘记密码路由与账户编辑提示 6. 优化文件传输与字体导入逻辑,废弃过时的bytes属性使用 7. 添加Spotlight全局快捷键支持,更新隐私权限与通知配置 8. 补充数据库迁移脚本与部署文档,修复后端接口兼容问题 9. 调整部分UI交互细节,优化内存占用与应用稳定性
This commit is contained in:
@@ -1,11 +1,15 @@
|
||||
/// @name 任务卡片组件
|
||||
/// @date 2026-05-14
|
||||
/// @desc 每日任务卡片: 图标+名称+进度条+领取按钮
|
||||
/// @update v5.8 替换硬编码CupertinoColors为AppTheme统一主题色
|
||||
/// @update v6.0 替换emoji为CupertinoIcons, 使用AppTypography/AppSpacing/AppRadius
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../core/theme/app_theme.dart';
|
||||
import '../../../core/theme/app_spacing.dart';
|
||||
import '../../../core/theme/app_typography.dart';
|
||||
import '../../../core/theme/app_radius.dart';
|
||||
|
||||
class TaskCard extends StatelessWidget {
|
||||
final String icon;
|
||||
@@ -42,17 +46,17 @@ class TaskCard extends StatelessWidget {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
||||
padding: const EdgeInsets.all(14),
|
||||
margin: const EdgeInsets.symmetric(
|
||||
horizontal: AppSpacing.md, vertical: AppSpacing.xs,
|
||||
),
|
||||
padding: const EdgeInsets.all(AppSpacing.sm + AppSpacing.xs),
|
||||
decoration: BoxDecoration(
|
||||
color: ext.bgCard,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
borderRadius: AppRadius.lgBorder,
|
||||
border: Border.all(
|
||||
color: completed
|
||||
? ext.successColor.withValues(alpha: 0.3)
|
||||
: ext.isDark
|
||||
? CupertinoColors.separator.withValues(alpha: 0.3)
|
||||
: CupertinoColors.separator.withValues(alpha: 0.5),
|
||||
: ext.textHint.withValues(alpha: 0.15),
|
||||
width: completed ? 1.5 : 0.5,
|
||||
),
|
||||
boxShadow: [
|
||||
@@ -69,16 +73,17 @@ class TaskCard extends StatelessWidget {
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
// ---- 任务图标 ----
|
||||
Text(icon, style: const TextStyle(fontSize: 26)),
|
||||
const SizedBox(width: 10),
|
||||
const SizedBox(width: AppSpacing.sm),
|
||||
// ---- 名称+进度 ----
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
name,
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
style: AppTypography.callout.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: completed
|
||||
? ext.textSecondary
|
||||
@@ -91,66 +96,26 @@ class TaskCard extends StatelessWidget {
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
'$progress/$target',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
style: AppTypography.caption1.copyWith(
|
||||
color: ext.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// ---- 状态标签 ----
|
||||
if (claimed)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 10,
|
||||
vertical: 4,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: ext.isDark
|
||||
? CupertinoColors.systemGrey.withValues(alpha: 0.2)
|
||||
: ext.bgSecondary,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(
|
||||
'✅ 已领取',
|
||||
style: TextStyle(fontSize: 12, color: ext.textSecondary),
|
||||
),
|
||||
)
|
||||
_buildClaimedTag(ext)
|
||||
else if (completed)
|
||||
CupertinoButton(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 14,
|
||||
vertical: 4,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
color: ext.warningColor,
|
||||
onPressed: onClaim,
|
||||
child: Text(
|
||||
'领取 +$expReward💎+$scoreReward⭐',
|
||||
style: TextStyle(fontSize: 12, color: ext.textOnAccent),
|
||||
),
|
||||
minimumSize: const Size(28, 28),
|
||||
)
|
||||
_buildClaimButton(ext)
|
||||
else
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 10,
|
||||
vertical: 4,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: ext.infoColor.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(
|
||||
'+$expReward💎 +$scoreReward⭐',
|
||||
style: TextStyle(fontSize: 11, color: ext.infoColor),
|
||||
),
|
||||
),
|
||||
_buildRewardPreview(ext),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const SizedBox(height: AppSpacing.sm),
|
||||
// ---- 进度条 ----
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
borderRadius: AppRadius.xsBorder,
|
||||
child: LinearProgressIndicator(
|
||||
value: percent / 100.0,
|
||||
backgroundColor: ext.isDark ? ext.bgSecondary : ext.bgElevated,
|
||||
@@ -165,4 +130,99 @@ class TaskCard extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ---- 已领取标签 ----
|
||||
|
||||
Widget _buildClaimedTag(AppThemeExtension ext) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: AppSpacing.sm, vertical: AppSpacing.xs,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: ext.isDark
|
||||
? ext.iconTintGrey.withValues(alpha: 0.2)
|
||||
: ext.bgSecondary,
|
||||
borderRadius: AppRadius.smBorder,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
CupertinoIcons.checkmark_circle_fill,
|
||||
size: 14,
|
||||
color: ext.successColor,
|
||||
),
|
||||
const SizedBox(width: AppSpacing.xs),
|
||||
Text(
|
||||
'已领取',
|
||||
style: AppTypography.caption1.copyWith(
|
||||
color: ext.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ---- 领取按钮 ----
|
||||
|
||||
Widget _buildClaimButton(AppThemeExtension ext) {
|
||||
return CupertinoButton(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: AppSpacing.sm + AppSpacing.xs, vertical: AppSpacing.xs,
|
||||
),
|
||||
borderRadius: AppRadius.smBorder,
|
||||
color: ext.warningColor,
|
||||
onPressed: onClaim,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
CupertinoIcons.gift_fill,
|
||||
size: 13,
|
||||
color: ext.textOnAccent,
|
||||
),
|
||||
const SizedBox(width: AppSpacing.xs),
|
||||
Text(
|
||||
'+$expReward +$scoreReward',
|
||||
style: AppTypography.caption1.copyWith(
|
||||
color: ext.textOnAccent,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
minimumSize: const Size(28, 28),
|
||||
);
|
||||
}
|
||||
|
||||
// ---- 奖励预览 ----
|
||||
|
||||
Widget _buildRewardPreview(AppThemeExtension ext) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: AppSpacing.sm, vertical: AppSpacing.xs,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: ext.infoColor.withValues(alpha: 0.1),
|
||||
borderRadius: AppRadius.smBorder,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
CupertinoIcons.star_fill,
|
||||
size: 12,
|
||||
color: ext.infoColor,
|
||||
),
|
||||
const SizedBox(width: AppSpacing.xs),
|
||||
Text(
|
||||
'+$expReward +$scoreReward',
|
||||
style: AppTypography.caption2.copyWith(
|
||||
color: ext.infoColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,39 +1,25 @@
|
||||
/// ============================================================
|
||||
/// 闲言APP — 安全图表包装组件
|
||||
/// 创建时间: 2026-06-05
|
||||
/// 更新时间: 2026-06-05
|
||||
/// 更新时间: 2026-06-07
|
||||
/// 作用: 统一处理 Syncfusion 图表生命周期,解决
|
||||
/// RenderChartFadeTransition disposed 错误,
|
||||
/// 在组件 dispose 后阻止图表更新,防止页面快速切换时卡死
|
||||
/// 上次更新: 初始创建,从 DeferredBuilder 演进而来
|
||||
/// 上次更新: 最终修复方案——
|
||||
/// 用嵌套 SafeChartContainer StatefulWidget 包裹图表,
|
||||
/// 在 SafeChartWidget deactivate 时通过 GlobalKey
|
||||
/// 调用 SafeChartContainerState.disposeInternal(),
|
||||
/// 主动 dispose 图表内部的所有 RenderObject,
|
||||
/// 确保 unmount 时不会访问已 disposed 的 RenderObject
|
||||
/// ============================================================
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
|
||||
/// 安全图表包装组件 — 统一处理 Syncfusion 图表生命周期
|
||||
///
|
||||
/// 与 DeferredBuilder 的区别:
|
||||
/// - 专为 Syncfusion 图表场景设计,内置更严格的 disposed 检查
|
||||
/// - 延迟渲染 + RepaintBoundary + disposed 三重保护
|
||||
/// - 支持 chartName 用于日志标识,方便追踪问题
|
||||
/// - placeholder 默认使用 CupertinoActivityIndicator,更符合 iOS 风格
|
||||
///
|
||||
/// 使用方式:
|
||||
/// ```dart
|
||||
/// SafeChartWidget(
|
||||
/// chartName: '学习进度',
|
||||
/// placeholder: const SizedBox(height: 200, child: Center(child: CupertinoActivityIndicator())),
|
||||
/// chartBuilder: (_) => SfCartesianChart(series: [...]),
|
||||
/// )
|
||||
/// ```
|
||||
class SafeChartWidget extends StatefulWidget {
|
||||
/// 图表构建器,返回 Syncfusion 图表组件
|
||||
final Widget Function(BuildContext context) chartBuilder;
|
||||
|
||||
/// 首帧占位组件,默认居中 CupertinoActivityIndicator
|
||||
final Widget? placeholder;
|
||||
|
||||
/// 图表名称,用于日志标识,方便追踪 disposed 错误来源
|
||||
final String? chartName;
|
||||
|
||||
const SafeChartWidget({
|
||||
@@ -48,26 +34,47 @@ class SafeChartWidget extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _SafeChartWidgetState extends State<SafeChartWidget> {
|
||||
/// 是否已 disposed
|
||||
bool _disposed = false;
|
||||
|
||||
/// 是否已准备好渲染图表(延迟到 postFrameCallback 后)
|
||||
bool _ready = false;
|
||||
bool _wasCurrent = true;
|
||||
final GlobalKey<_ChartContainerState> _containerKey = GlobalKey();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// 延迟渲染图表,避免在 build 阶段触发 markNeedsLayout
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
SchedulerBinding.instance.addPostFrameCallback((_) {
|
||||
if (!_disposed && mounted) {
|
||||
setState(() => _ready = true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
if (_disposed) return;
|
||||
|
||||
final isCurrent = ModalRoute.of(context)?.isCurrent ?? true;
|
||||
|
||||
if (_wasCurrent && !isCurrent && _ready) {
|
||||
_wasCurrent = false;
|
||||
_ready = false;
|
||||
// 立即 setState 移除图表
|
||||
setState(() {});
|
||||
return;
|
||||
}
|
||||
|
||||
_wasCurrent = isCurrent;
|
||||
}
|
||||
|
||||
@override
|
||||
void deactivate() {
|
||||
_ready = false;
|
||||
super.deactivate();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
// 标记已 disposed,阻止后续任何 setState 和图表渲染
|
||||
_disposed = true;
|
||||
_ready = false;
|
||||
super.dispose();
|
||||
@@ -75,13 +82,39 @@ class _SafeChartWidgetState extends State<SafeChartWidget> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// disposed 后不渲染图表,防止 RenderChartFadeTransition disposed 错误
|
||||
if (_disposed || !_ready) {
|
||||
return widget.placeholder ??
|
||||
const Center(child: CupertinoActivityIndicator());
|
||||
return widget.placeholder ?? const SizedBox.shrink();
|
||||
}
|
||||
// RepaintBoundary 隔离图表重绘,避免波及父级
|
||||
return RepaintBoundary(
|
||||
child: _ChartContainer(
|
||||
key: _containerKey,
|
||||
chartBuilder: widget.chartBuilder,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 内部容器——用 TickerMode + 条件渲染包裹图表
|
||||
class _ChartContainer extends StatefulWidget {
|
||||
final Widget Function(BuildContext context) chartBuilder;
|
||||
|
||||
const _ChartContainer({super.key, required this.chartBuilder});
|
||||
|
||||
@override
|
||||
State<_ChartContainer> createState() => _ChartContainerState();
|
||||
}
|
||||
|
||||
class _ChartContainerState extends State<_ChartContainer>
|
||||
with AutomaticKeepAliveClientMixin {
|
||||
@override
|
||||
bool get wantKeepAlive => false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
// TickerMode 确保 deactivate 时子树 ticker 被禁用
|
||||
return TickerMode(
|
||||
enabled: true,
|
||||
child: widget.chartBuilder(context),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
/// ============================================================
|
||||
/// 闲言APP — 延迟渲染包装组件
|
||||
/// 创建时间: 2026-06-02
|
||||
/// 更新时间: 2026-06-05
|
||||
/// 更新时间: 2026-06-06
|
||||
/// 作用: 将子组件(如syncfusion chart)延迟到postFrameCallback渲染
|
||||
/// 避免chart在build阶段触发markNeedsLayout导致卡死/闪退
|
||||
/// 内置RepaintBoundary隔离重绘,避免图表重绘波及父级
|
||||
/// 上次更新: 增加dispose保护,防止RenderChartFadeTransition disposed错误
|
||||
/// 上次更新: 增强dispose保护,deactivate时立即替换为占位符防止unmount阶段markNeedsLayout
|
||||
/// ============================================================
|
||||
|
||||
import 'package:flutter/widgets.dart';
|
||||
@@ -39,6 +39,15 @@ class _DeferredBuilderState extends State<DeferredBuilder> {
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void deactivate() {
|
||||
// 在deactivate阶段(从树中移除但尚未dispose)立即停止渲染图表
|
||||
// 防止后续unmount阶段Syncfusion内部CustomLayoutBuilderElement
|
||||
// 触发已disposed的RenderChartFadeTransition.markNeedsLayout
|
||||
_ready = false;
|
||||
super.deactivate();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_disposed = true;
|
||||
|
||||
130
lib/shared/widgets/feedback/login_guard_widget.dart
Normal file
130
lib/shared/widgets/feedback/login_guard_widget.dart
Normal file
@@ -0,0 +1,130 @@
|
||||
/// ============================================================
|
||||
/// 闲言APP — 登录守卫组件
|
||||
/// 创建时间: 2026-06-07
|
||||
/// 更新时间: 2026-06-07
|
||||
/// 作用: 未登录时显示提示+登录按钮,已登录时显示正常内容
|
||||
/// 上次更新: 接入i18n翻译、新增onLogin回调
|
||||
/// ============================================================
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../../core/router/app_nav_extension.dart';
|
||||
import '../../../core/router/app_routes.dart';
|
||||
import '../../../core/theme/app_spacing.dart';
|
||||
import '../../../core/theme/app_theme.dart';
|
||||
import '../../../core/theme/app_typography.dart';
|
||||
import '../../../core/theme/app_radius.dart';
|
||||
import '../../../features/auth/providers/auth_provider.dart';
|
||||
import '../../../l10n/translations.dart';
|
||||
import '../containers/glass_container.dart';
|
||||
|
||||
/// 登录守卫组件 — 未登录时展示提示视图,已登录时展示 child
|
||||
///
|
||||
/// 用法:
|
||||
/// ```dart
|
||||
/// LoginGuardWidget(
|
||||
/// child: MyPageContent(),
|
||||
/// )
|
||||
/// ```
|
||||
class LoginGuardWidget extends ConsumerWidget {
|
||||
const LoginGuardWidget({
|
||||
super.key,
|
||||
required this.child,
|
||||
this.icon = CupertinoIcons.person_crop_circle_badge_xmark,
|
||||
this.title,
|
||||
this.subtitle,
|
||||
this.buttonText,
|
||||
this.onLogin,
|
||||
});
|
||||
|
||||
/// 已登录时展示的内容
|
||||
final Widget child;
|
||||
|
||||
/// 未登录提示图标
|
||||
final IconData icon;
|
||||
|
||||
/// 未登录提示标题(默认: 使用i18n翻译)
|
||||
final String? title;
|
||||
|
||||
/// 未登录提示副标题(默认: 登录后即可查看个人信息和数据)
|
||||
final String? subtitle;
|
||||
|
||||
/// 登录按钮文字(默认: 使用i18n翻译)
|
||||
final String? buttonText;
|
||||
|
||||
/// 登录成功后的回调(可选)
|
||||
final VoidCallback? onLogin;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final authState = ref.watch(authProvider);
|
||||
|
||||
// 未初始化时显示加载
|
||||
if (!authState.isInitialized) {
|
||||
return const Center(child: CupertinoActivityIndicator());
|
||||
}
|
||||
|
||||
// 已登录时显示正常内容
|
||||
if (authState.isLoggedIn) {
|
||||
return child;
|
||||
}
|
||||
|
||||
// 未登录时显示提示视图
|
||||
return _buildLoggedOutView(context, ref);
|
||||
}
|
||||
|
||||
Widget _buildLoggedOutView(BuildContext context, WidgetRef ref) {
|
||||
final ext = AppTheme.ext(context);
|
||||
final t = ref.watch(translationsProvider);
|
||||
// 使用 i18n 翻译,fallback 到中文
|
||||
final displayTitle = title ?? t.profile.loginToViewProfile;
|
||||
final displaySubtitle = subtitle ?? '登录后即可查看个人信息和数据';
|
||||
final displayButtonText = buttonText ?? t.profile.goLogin;
|
||||
|
||||
return Semantics(
|
||||
label: '$displayTitle. $displaySubtitle',
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: AppSpacing.md,
|
||||
vertical: AppSpacing.xxl,
|
||||
),
|
||||
child: GlassContainer(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(icon, size: 48, color: ext.textHint),
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
Text(
|
||||
displayTitle,
|
||||
style: AppTypography.title3.copyWith(color: ext.textPrimary),
|
||||
),
|
||||
const SizedBox(height: AppSpacing.sm),
|
||||
Text(
|
||||
displaySubtitle,
|
||||
style: AppTypography.subhead.copyWith(color: ext.textSecondary),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
CupertinoButton(
|
||||
color: ext.accent,
|
||||
borderRadius: AppRadius.pillBorder,
|
||||
onPressed: () {
|
||||
context.appPush(AppRoutes.login);
|
||||
onLogin?.call();
|
||||
},
|
||||
child: Text(
|
||||
displayButtonText,
|
||||
style: AppTypography.body.copyWith(
|
||||
color: CupertinoColors.white,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user