chore: v6.6.6 版本迭代更新

主要变更:
1. 重构"国学"相关模块为"经典名句",统一命名规范
2. 重命名"阅读报告"为"使用报告",调整相关文案与配置
3. 修复iOS模拟器图片缓存兼容问题,优化图表渲染逻辑
4. 新增设备活跃状态前端兜底判断,修复在线计数异常
5. 完善登录/注册流程,新增忘记密码路由与账户编辑提示
6. 优化文件传输与字体导入逻辑,废弃过时的bytes属性使用
7. 添加Spotlight全局快捷键支持,更新隐私权限与通知配置
8. 补充数据库迁移脚本与部署文档,修复后端接口兼容问题
9. 调整部分UI交互细节,优化内存占用与应用稳定性
This commit is contained in:
Developer
2026-06-07 06:56:52 +08:00
parent e119c84868
commit f281e465bb
159 changed files with 10502 additions and 2120 deletions

View File

@@ -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,
),
),
],
),
);
}
}

View File

@@ -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),
);
}

View File

@@ -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;

View 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,
),
),
),
],
),
),
),
);
}
}