Files
xianyan/docs/spec/sentence_detail_refactor_plan.md
Developer 182735df3b release: 发布v6.6.2版本,优化多项功能与体验
本次更新包含:
1.  更新应用标语与隐私政策文案,调整品牌宣传语
2.  重构Feed ID解析、HTML清理工具类,提取重复逻辑
3.  新增全屏图片查看器、通用动画操作按钮组件
4.  修复电池监听空指针、快捷操作异常捕获问题
5.  优化搜索、会话列表、RSS阅读器等页面体验
6.  完善多语言支持,新增多个翻译模块
7.  移除冗余代码,统一数字格式化逻辑
8.  调整登录页面布局与交互逻辑
2026-06-01 08:16:01 +08:00

27 KiB
Raw Blame History

句子详情面板全面重构 实施计划

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: 对句子详情面板进行 15 项改进涵盖架构重构、Bug修复、新功能、代码质量提升

Architecture: 以 Riverpod Provider 为核心状态管理,提取通用组件到 shared/widgets工具函数收敛到 core/utils消除 prop drilling 和重复代码

Tech Stack: Flutter + Riverpod + Cupertino (iOS风格)


文件变更清单

新建文件

文件路径 职责
lib/core/utils/data/html_utils.dart HTML清理通用工具
lib/core/utils/data/feed_id_utils.dart FeedId解析值对象
lib/shared/widgets/interaction/animated_action_button.dart 通用动画操作按钮
lib/shared/widgets/media/full_screen_photo_view.dart 通用全屏图片查看器
lib/features/home/presentation/panels/sentence_detail_provider.dart 句子详情Riverpod Provider

修改文件

文件路径 变更内容
lib/features/home/providers/home_sentence_model.dart 增加 feedId 字段
lib/features/home/presentation/panels/sentence_detail_panel.dart 使用 Provider + 修复 Bug + 增强功能
lib/features/home/presentation/panels/sentence_detail_content.dart 使用 Provider + 通用组件 + 硬编码收敛
lib/features/home/presentation/panels/sentence_detail_actions.dart 使用 Provider + 通用组件 + Bug修复
lib/features/home/presentation/providers/sentence_detail_sheet.dart 使用通用工具替换重复代码
lib/features/home/presentation/home_sentence_card.dart 使用 NumberFormatter 替换 _fmtCount
lib/features/source/presentation/source_widgets.dart 使用 NumberFormatter 替换 fmtCount
lib/features/discover/presentation/pages/tool/rss_reader_page.dart 使用 HtmlUtils 替换 _stripHtmlTags
CHANGELOG.md 记录变更

Task 1: 提取 HtmlUtils 通用工具

Files:

  • Create: lib/core/utils/data/html_utils.dart

  • Modify: lib/features/home/presentation/panels/sentence_detail_content.dart

  • Modify: lib/features/home/presentation/providers/sentence_detail_sheet.dart

  • Modify: lib/features/discover/presentation/pages/tool/rss_reader_page.dart

  • Step 1: 创建 html_utils.dart

/// ============================================================
/// 闲言APP — HTML清理通用工具
/// 创建时间: 2026-06-01
/// 更新时间: 2026-06-01
/// 作用: 提供统一的HTML标签清理和图片URL提取方法
/// 上次更新: 从sentence_detail_content.dart和sentence_detail_sheet.dart提取
/// ============================================================

import 'pattern_utils.dart';

class HtmlUtils {
  HtmlUtils._();

  /// 清除HTML标签保留纯文本内容
  static String stripTags(String html) {
    if (html.isEmpty) return html;
    var result = html;
    result = result.replaceAll(regex(r'<br\s*/?>', caseSensitive: false), '\n');
    result = result.replaceAll(regex(r'<p\s*/?>', caseSensitive: false), '\n');
    result = result.replaceAll(regex(r'</p>', caseSensitive: false), '');
    result = result.replaceAll(
      regex(r'<strong[^>]*>(.*?)</strong>', caseSensitive: false, dotAll: true),
      r'$1',
    );
    result = result.replaceAll(
      regex(r'<em[^>]*>(.*?)</em>', caseSensitive: false, dotAll: true),
      r'$1',
    );
    result = result.replaceAll(regex(r'<[^>]*>'), '');
    result = result.replaceAll(regex(r'\n{3,}'), '\n\n');
    return result.trim();
  }

  /// 从HTML中提取所有img标签的src URL
  static List<String> extractImageUrls(String html) {
    final imgRegex = regex(
      r'<img[^>]+src\s*=\s*["\x27]([^"\x27]+)["\x27]',
      dotAll: true,
    );
    return imgRegex
        .allMatches(html)
        .map((m) => m.group(1) ?? '')
        .where((url) => url.isNotEmpty)
        .toList();
  }
}
  • Step 2: 在 sentence_detail_content.dart 中替换

删除文件底部的 _stripHtmlTags_extractImageUrls 函数,替换为 HtmlUtils.stripTagsHtmlUtils.extractImageUrls 调用。添加 import:

import '../../../../core/utils/data/html_utils.dart';
  • Step 3: 在 sentence_detail_sheet.dart 中替换

删除文件中的 _stripHtmlTags 方法,替换为 HtmlUtils.stripTags 调用。添加 import:

import 'package:xianyan/core/utils/data/html_utils.dart';
  • Step 4: 在 rss_reader_page.dart 中替换

删除文件中的两处 _stripHtmlTags 方法定义,替换为 HtmlUtils.stripTags 调用。添加 import:

import 'package:xianyan/core/utils/data/html_utils.dart';
  • Step 5: 运行 Dart analyze 验证

Run: dart analyze lib/core/utils/data/html_utils.dart lib/features/home/presentation/panels/sentence_detail_content.dart lib/features/home/presentation/providers/sentence_detail_sheet.dart lib/features/discover/presentation/pages/tool/rss_reader_page.dart Expected: No issues found


Task 2: 提取 FeedIdUtils 值对象

Files:

  • Create: lib/core/utils/data/feed_id_utils.dart

  • Modify: lib/features/home/providers/home_sentence_model.dart — 增加 feedId 字段

  • Modify: lib/features/home/presentation/panels/sentence_detail_panel.dart — 使用 FeedIdUtils

  • Modify: lib/features/home/presentation/providers/sentence_detail_sheet.dart — 使用 FeedIdUtils

  • Modify: lib/features/home/presentation/providers/sentence_dialogs.dart — 使用 FeedIdUtils

  • Step 1: 创建 feed_id_utils.dart

/// ============================================================
/// 闲言APP — FeedId解析工具
/// 创建时间: 2026-06-01
/// 更新时间: 2026-06-01
/// 作用: 统一FeedId解析逻辑提供类型安全的ID提取
/// 上次更新: 从多处_extractFeedId重复实现中提取
/// ============================================================

class FeedIdUtils {
  FeedIdUtils._();

  /// 从复合ID中提取数字FeedId
  /// 复合ID格式: "{feedType}_{numericId}" 或纯数字
  /// 返回0表示解析失败
  static int extract(String id) {
    if (id.isEmpty) return 0;
    if (id.contains('_')) {
      return int.tryParse(id.split('_').last) ?? 0;
    }
    return int.tryParse(id) ?? 0;
  }

  /// 判断ID是否有效大于0
  static bool isValid(String id) => extract(id) > 0;

  /// 从复合ID中提取feedType部分
  static String extractType(String id) {
    if (id.contains('_')) {
      return id.substring(0, id.indexOf('_'));
    }
    return '';
  }
}
  • Step 2: 在 HomeSentence 模型中增加 feedId 字段

home_sentence_model.dartHomeSentence 类中:

  • 增加字段: final int feedId; 默认值为 0

  • 在构造函数中增加: this.feedId = 0

  • fromFeedItem 工厂方法中: feedId: item.id

  • fromDb 工厂方法中: feedId: 0 (本地缓存无数字ID)

  • fromHitokoto 工厂方法中: feedId: quote.id

  • copyWith 中: int? feedId,feedId: feedId ?? this.feedId

  • Step 3: 在 sentence_detail_panel.dart 中使用 FeedIdUtils

删除 _extractFeedId 方法,替换所有调用为 FeedIdUtils.extract(sentence.id)。 添加 import: import '../../../../core/utils/data/feed_id_utils.dart';

  • Step 4: 在 sentence_detail_sheet.dart 中使用 FeedIdUtils

删除 _extractFeedId 方法,替换所有调用为 FeedIdUtils.extract(sentence.id)

  • Step 5: 在 sentence_dialogs.dart 中使用 FeedIdUtils

删除 _extractFeedId 方法,替换为 FeedIdUtils.extract(sentence.id)

  • Step 6: 运行 Dart analyze 验证

Run: dart analyze lib/core/utils/data/feed_id_utils.dart lib/features/home/providers/home_sentence_model.dart lib/features/home/presentation/panels/ lib/features/home/presentation/providers/sentence_detail_sheet.dart lib/features/home/presentation/providers/sentence_dialogs.dart Expected: No issues found


Task 3: 替换 _fmtCount 为 NumberFormatter

Files:

  • Modify: lib/features/home/presentation/panels/sentence_detail_content.dart

  • Modify: lib/features/home/presentation/providers/sentence_detail_sheet.dart

  • Modify: lib/features/home/presentation/home_sentence_card.dart

  • Step 1: 在 sentence_detail_content.dart 中替换

删除文件底部的 _fmtCount 函数,替换所有调用为 NumberFormatter.formatCount。 添加 import: import '../../../../core/utils/data/number_formatter.dart';

  • Step 2: 在 sentence_detail_sheet.dart 中替换

删除 _fmtCount 方法,替换为 NumberFormatter.formatCount。 添加 import: import 'package:xianyan/core/utils/data/number_formatter.dart';

  • Step 3: 在 home_sentence_card.dart 中替换

删除 _fmtCount 方法,替换为 NumberFormatter.formatCount。 添加 import: import '../../../../core/utils/data/number_formatter.dart';

  • Step 4: 运行 Dart analyze 验证

Task 4: 提取 AnimatedActionButton 通用组件

Files:

  • Create: lib/shared/widgets/interaction/animated_action_button.dart

  • Modify: lib/features/home/presentation/panels/sentence_detail_actions.dart

  • Step 1: 创建 animated_action_button.dart

_PanelActionBtn + _PanelActionBtnState 提取为公开的 AnimatedActionButton,支持:

  • emoji (String) — 显示的emoji

  • label (String) — 标签文字

  • isActive (bool) — 是否激活

  • onTap (VoidCallback) — 点击回调

  • activeEmojiMap (Map<String, String>?) — 自定义激活emoji映射默认 {'👍':'❤️','':'🌟','📖':''}

  • ext (AppThemeExtension) — 主题扩展

  • Step 2: 在 sentence_detail_actions.dart 中使用 AnimatedActionButton

删除 _PanelActionBtn_PanelActionBtnState,替换为 AnimatedActionButton。 添加 import: import '../../../../shared/widgets/interaction/animated_action_button.dart';

  • Step 3: 运行 Dart analyze 验证

Task 5: 提取 FullScreenPhotoView 通用组件

Files:

  • Create: lib/shared/widgets/media/full_screen_photo_view.dart

  • Modify: lib/features/home/presentation/panels/sentence_detail_content.dart

  • Modify: lib/features/home/presentation/panels/sentence_detail_panel.dart

  • Step 1: 创建 full_screen_photo_view.dart

FullScreenPhotoViewsentence_detail_content.dart 移动到 lib/shared/widgets/media/full_screen_photo_view.dart,代码不变。

  • Step 2: 在 sentence_detail_content.dart 中删除 FullScreenPhotoView 类

删除 FullScreenPhotoView 类定义,添加 import:

import '../../../../shared/widgets/media/full_screen_photo_view.dart';
  • Step 3: 在 sentence_detail_panel.dart 中更新 import

确保 _showPhotoView 方法中的 FullScreenPhotoView 引用通过新路径导入。

  • Step 4: 运行 Dart analyze 验证

Task 6: 创建句子详情 Riverpod Provider

Files:

  • Create: lib/features/home/presentation/panels/sentence_detail_provider.dart

  • Modify: lib/features/home/presentation/panels/sentence_detail_panel.dart

  • Modify: lib/features/home/presentation/panels/sentence_detail_content.dart

  • Modify: lib/features/home/presentation/panels/sentence_detail_actions.dart

  • Step 1: 创建 sentence_detail_provider.dart

/// ============================================================
/// 闲言APP — 句子详情Provider
/// 创建时间: 2026-06-01
/// 更新时间: 2026-06-01
/// 作用: 句子详情面板的状态管理消除prop drilling
/// 上次更新: 初始创建
/// ============================================================

import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../home/models/feed_model.dart';
import '../../../home/services/feed_service.dart';
import '../../../home/providers/home_provider.dart';
import '../../../../core/utils/data/feed_id_utils.dart';
import '../../../../core/utils/logger.dart';

/// 句子详情数据状态
class SentenceDetailState {
  const SentenceDetailState({
    this.detailItem,
    this.loadingDetail = false,
    this.loadError,
    this.showTtsPlayer = false,
  });

  final FeedItem? detailItem;
  final bool loadingDetail;
  final String? loadError;
  final bool showTtsPlayer;

  SentenceDetailState copyWith({
    FeedItem? detailItem,
    bool? loadingDetail,
    String? loadError,
    bool? showTtsPlayer,
    bool clearError = false,
    bool clearDetailItem = false,
  }) {
    return SentenceDetailState(
      detailItem: clearDetailItem ? null : (detailItem ?? this.detailItem),
      loadingDetail: loadingDetail ?? this.loadingDetail,
      loadError: clearError ? null : (loadError ?? this.loadError),
      showTtsPlayer: showTtsPlayer ?? this.showTtsPlayer,
    );
  }
}

/// 句子详情Notifier
class SentenceDetailNotifier extends Notifier<SentenceDetailState> {
  @override
  SentenceDetailState build() => const SentenceDetailState();

  /// 加载详情数据
  Future<void> loadDetail(HomeSentence sentence) async {
    if (state.detailItem != null || state.loadingDetail) return;

    final effectiveType = sentence.feedType ?? sentence.type;
    if (effectiveType == null || effectiveType.isEmpty) return;

    final feedId = FeedIdUtils.extract(sentence.id);
    if (feedId <= 0) return;

    state = state.copyWith(loadingDetail: true, clearError: true);
    try {
      final detail = await FeedService.fetchDetail(
        type: effectiveType,
        id: feedId,
      ).timeout(const Duration(seconds: 6), onTimeout: () => null);
      if (detail != null) {
        state = state.copyWith(
          detailItem: detail,
          loadingDetail: false,
          clearError: true,
        );
      } else {
        state = state.copyWith(
          loadingDetail: false,
          loadError: '详情数据为空',
        );
      }
    } catch (e) {
      Log.e('详情加载失败: ${sentence.id}', e);
      state = state.copyWith(
        loadingDetail: false,
        loadError: '加载失败: $e',
      );
    }
  }

  /// 重试加载
  Future<void> retryLoad(HomeSentence sentence) async {
    state = state.copyWith(clearDetailItem: true, clearError: true);
    await loadDetail(sentence);
  }

  /// 切换TTS播放器
  void toggleTtsPlayer(bool show) {
    state = state.copyWith(showTtsPlayer: show);
  }

  /// 关闭TTS播放器
  void hideTtsPlayer() {
    state = state.copyWith(showTtsPlayer: false);
  }
}

/// 句子详情Provider — 以句子ID为key
final sentenceDetailProvider = NotifierProvider<SentenceDetailNotifier, SentenceDetailState>(
  SentenceDetailNotifier.new,
);
  • Step 2: 重构 sentence_detail_panel.dart

  • 删除 _detailItem, _loadingDetail, _showTtsPlayer 状态变量

  • 使用 ref.watch(sentenceDetailProvider) 获取状态

  • initState 中调用 ref.read(sentenceDetailProvider.notifier).loadDetail(sentence)

  • _loadDetailIfNeeded 方法删除,逻辑移入 Provider

  • _toggleTtsPlayer 改为调用 ref.read(sentenceDetailProvider.notifier).toggleTtsPlayer(true)

  • 不再通过构造函数传递 detailItem, loadingDetail, showTtsPlayer 给子组件

  • Step 3: 重构 sentence_detail_content.dart

  • 改为 ConsumerWidget,通过 ref.watch(sentenceDetailProvider) 读取 detailItemloadingDetail

  • 删除构造函数中的 detailItemloadingDetail 参数

  • 保留 sentence, accentColor, onImageTap 参数(这些是面板级配置)

  • Step 4: 重构 sentence_detail_actions.dart

  • 改为 ConsumerWidget,通过 ref.watch(sentenceDetailProvider) 读取 showTtsPlayer

  • 删除构造函数中的 showTtsPlayer 参数

  • 保留 sentence, feedId, targetType, onClosePanel, onToggleTts 参数

  • Step 5: 运行 Dart analyze 验证


Task 7: 修复点赞/收藏状态反馈 Bug

Files:

  • Modify: lib/features/home/presentation/panels/sentence_detail_actions.dart

  • Step 1: 修复点赞Toast

将:

ref.read(homeProvider.notifier).toggleLike(sentence.id);
HapticService.light();
AppToast.showSuccess(sentence.isLiked ? '已取消点赞' : '👍 已点赞');

改为:

final wasLiked = sentence.isLiked;
ref.read(homeProvider.notifier).toggleLike(sentence.id);
HapticService.light();
AppToast.showSuccess(wasLiked ? '已取消点赞' : '👍 已点赞');
  • Step 2: 修复收藏Toast

收藏已有 wasFavorited 变量,逻辑正确,无需修改。确认代码:

final wasFavorited = sentence.isFavorited;
ref.read(homeProvider.notifier).toggleFavorite(sentence.id);
HapticService.light();
AppToast.showSuccess(wasFavorited ? '已取消收藏' : '⭐ 已收藏');
  • Step 3: 运行 Dart analyze 验证

Task 8: 添加加载失败重试

Files:

  • Modify: lib/features/home/presentation/panels/sentence_detail_content.dart

  • Step 1: 在 SentenceDetailContent 中增加错误状态UI

_buildSentenceCard 方法中,当 loadingDetail 为 false 且 detailItem 为 null 且有 loadError 时,显示错误提示 + 重试按钮:

if (displayText.isEmpty && !loadingDetail) {
  final error = ref.watch(sentenceDetailProvider).loadError;
  if (error != null) {
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: AppSpacing.md),
      child: Center(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            Icon(CupertinoIcons.exclamationmark_triangle, color: ext.textHint, size: 32),
            const SizedBox(height: AppSpacing.sm),
            Text(error, style: AppTypography.caption1.copyWith(color: ext.textHint)),
            const SizedBox(height: AppSpacing.sm),
            CupertinoButton(
              padding: const EdgeInsets.symmetric(horizontal: AppSpacing.md, vertical: AppSpacing.xs),
              borderRadius: AppRadius.mdBorder,
              color: ext.bgSecondary,
              onPressed: () => ref.read(sentenceDetailProvider.notifier).retryLoad(sentence),
              child: Text('重试', style: TextStyle(color: ext.accent)),
            ),
          ],
        ),
      ),
    );
  }
}
  • Step 2: 运行 Dart analyze 验证

Task 9: TTS播放器可关闭

Files:

  • Modify: lib/features/home/presentation/panels/sentence_detail_actions.dart

  • Modify: lib/features/home/presentation/panels/sentence_detail_provider.dart

  • Step 1: 在 Provider 中监听 TtsService 状态

SentenceDetailNotifier 中增加 TTS 完成监听:

StreamSubscription<TtsState>? _ttsSub;

@override
SentenceDetailState build() {
  ref.onDispose(() {
    _ttsSub?.cancel();
  });
  return const SentenceDetailState();
}

void _listenTtsState() {
  _ttsSub?.cancel();
  _ttsSub = TtsService.instance.onStateChanged.listen((ttsState) {
    if (ttsState == TtsState.idle && state.showTtsPlayer) {
      state = state.copyWith(showTtsPlayer: false);
    }
  });
}

toggleTtsPlayer 中调用 _listenTtsState()

  • Step 2: 在操作区增加关闭TTS按钮

showTtsPlayer 为 true 时,在 TtsPlayerBar 旁边增加关闭按钮:

if (widget.showTtsPlayer)
  Padding(
    padding: const EdgeInsets.only(top: AppSpacing.sm),
    child: Row(
      children: [
        Expanded(child: TtsPlayerBar(text: widget.sentence.text)),
        CupertinoButton(
          padding: const EdgeInsets.all(AppSpacing.sm),
          onPressed: () {
            TtsService.instance.stop();
            ref.read(sentenceDetailProvider.notifier).hideTtsPlayer();
          },
          child: Icon(CupertinoIcons.xmark_circle_fill, color: ext.textHint, size: 22),
        ),
      ],
    ),
  ),
  • Step 3: 运行 Dart analyze 验证

Task 10: 相关推荐功能

Files:

  • Create: lib/features/home/presentation/panels/sentence_detail_related.dart

  • Modify: lib/features/home/presentation/panels/sentence_detail_panel.dart

  • Modify: lib/features/home/presentation/panels/sentence_detail_provider.dart

  • Step 1: 在 Provider 中增加相关推荐状态和加载方法

// 在 SentenceDetailState 中增加:
final List<FeedItem> relatedItems;
final bool loadingRelated;

// 构造函数增加:
this.relatedItems = const [],
this.loadingRelated = false,

// copyWith 增加:
List<FeedItem>? relatedItems,
bool? loadingRelated,

// 在 Notifier 中增加:
Future<void> loadRelated(HomeSentence sentence) async {
  if (state.loadingRelated) return;
  final effectiveType = sentence.feedType ?? sentence.type;
  final feedId = FeedIdUtils.extract(sentence.id);
  if (effectiveType == null || effectiveType.isEmpty || feedId <= 0) return;

  state = state.copyWith(loadingRelated: true);
  try {
    final items = await FeedService.fetchRelatedRecommend(
      type: effectiveType, id: feedId, limit: 5,
    );
    state = state.copyWith(relatedItems: items, loadingRelated: false);
  } catch (e) {
    Log.e('相关推荐加载失败', e);
    state = state.copyWith(loadingRelated: false);
  }
}
  • Step 2: 创建 sentence_detail_related.dart

相关推荐卡片列表组件,使用 ConsumerWidget,通过 ref.watch(sentenceDetailProvider) 读取 relatedItemsloadingRelated。每个推荐项显示: feedIcon + feedName + title(截断) + 点赞数。

  • Step 3: 在 sentence_detail_panel.dart 中集成

SentenceDetailActions 下方添加 SentenceDetailRelated。 在 initState 中增加 ref.read(sentenceDetailProvider.notifier).loadRelated(sentence)

  • Step 4: 运行 Dart analyze 验证

Task 11: 句子卡片分享图片生成

Files:

  • Modify: lib/features/home/presentation/panels/sentence_detail_content.dart

  • Modify: lib/features/home/presentation/panels/sentence_detail_actions.dart

  • Step 1: 在句子卡片外层包裹 RepaintBoundary

final _cardKey = GlobalKey();

// 在 _buildSentenceCard 中:
RepaintBoundary(
  key: _cardKey,
  child: Heroine(...),
)
  • Step 2: 添加生成分享图方法
Future<void> _captureAndShare() async {
  try {
    final boundary = _cardKey.currentContext?.findRenderObject() as RenderRepaintBoundary?;
    if (boundary == null) return;
    final image = await boundary.toImage(pixelRatio: 3.0);
    final byteData = await image.toByteData(format: ImageByteFormat.png);
    if (byteData == null) return;
    // 使用 ShareSheet 分享图片
    ShareSheet.show(
      context: context,
      data: ShareData(
        text: sentence.text,
        author: sentence.author,
        source: sentence.feedName,
        id: sentence.id.toString(),
        title: '分享句子卡片',
        shareSource: '句子详情',
        cardStyle: ref.read(dailyCardStyleProvider),
        onResult: (result) => Log.i('分享卡片回调: ${result.label}'),
      ),
    );
  } catch (e) {
    Log.e('生成分享图失败', e);
    AppToast.showError('生成分享图失败');
  }
}
  • Step 3: 在快捷操作区增加"生成分享图"按钮

_buildQuickActions 的 Row 中,将分享按钮改为支持长按生成分享图。或者在句子卡片底部增加一个小的"📸 分享图"按钮。

  • Step 4: 运行 Dart analyze 验证

Task 12: 动画与过渡增强

Files:

  • Modify: lib/features/home/presentation/panels/sentence_detail_content.dart

  • Step 1: 添加入场动画

使用 SingleTickerProviderStateMixinImplicitlyAnimatedWidget 为内容区域各 section 添加交错入场动画:

// 在 SentenceDetailContent 中使用 TweenAnimationBuilder
// 每个section延迟50ms出现从下方淡入

SentenceDetailContentStatelessWidget 改为 StatefulWidget,使用 AnimationController + Staggered 动画。

  • Step 2: 运行 Dart analyze 验证

Task 13: 深色/浅色主题差异化

Files:

  • Modify: lib/features/home/presentation/panels/sentence_detail_content.dart

  • Modify: lib/features/home/presentation/panels/sentence_detail_actions.dart

  • Step 1: 句子卡片渐变 alpha 跟随主题

// 深色模式增大渐变alpha
colors: [
  accentColor.withValues(alpha: ext.isDark ? 0.15 : 0.08),
  accentColor.withValues(alpha: ext.isDark ? 0.05 : 0.02),
],
  • Step 2: 作者行渐变色跟随主题
gradient: LinearGradient(
  colors: ext.isDark
    ? [Color(0xFF0A84FF), Color(0xFF5E5CE6)]
    : [Color(0xFFFF9F0A), Color(0xFFFF375F)],
),
  • Step 3: 危险操作按钮深色模式差异化
color: ext.isDark
  ? ext.accent.withValues(alpha: 0.15)
  : ext.accent.withValues(alpha: 0.08),
  • Step 4: 运行 Dart analyze 验证

Task 14: 无障碍支持

Files:

  • Modify: lib/features/home/presentation/panels/sentence_detail_content.dart

  • Modify: lib/features/home/presentation/panels/sentence_detail_actions.dart

  • Modify: lib/shared/widgets/interaction/animated_action_button.dart

  • Step 1: SelectableText 添加 semanticsLabel

SelectableText(
  _stripHtmlTags(displayText),
  semanticsLabel: _stripHtmlTags(displayText),
  ...
)
  • Step 2: 操作按钮添加 Semantics

AnimatedActionButton 中:

Semantics(
  button: true,
  label: '$emoji $label${isActive ? ",已激活" : ""}',
  child: GestureDetector(...),
)
  • Step 3: 图片预览添加 Semantics
Semantics(
  image: true,
  label: '句子配图,点击查看大图',
  child: GestureDetector(...),
)
  • Step 4: 运行 Dart analyze 验证

Task 15: 硬编码魔法值收敛

Files:

  • Modify: lib/core/theme/app_theme.dart — 增加语义令牌

  • Modify: lib/features/home/presentation/panels/sentence_detail_content.dart

  • Modify: lib/features/home/presentation/panels/sentence_detail_actions.dart

  • Step 1: 在 AppThemeExtension 中增加语义令牌

// 句子详情面板专用令牌
double get sentenceQuoteFontSize => 48.0;
double get sentenceHintFontSize => 9.0;
double get sentenceImageMaxHeight => 240.0;
double get sentenceRelatedImageMaxHeight => 180.0;
double get sentenceCardGradientAlpha => isDark ? 0.15 : 0.08;
double get sentenceCardGradientAlphaEnd => isDark ? 0.05 : 0.02;
double get sentenceBorderAlpha => isDark ? 0.08 : 0.04;
double get sentenceActiveAlpha => 0.12;
double get sentenceActiveBorderAlpha => 0.4;
double get sentenceInactiveBorderAlpha => isDark ? 0.06 : 0.04;
double get sentenceDangerAlpha => isDark ? 0.15 : 0.08;
  • Step 2: 替换 sentence_detail_content.dart 中的硬编码值

将所有 fontSize: 48ext.sentenceQuoteFontSize 将所有 fontSize: 9ext.sentenceHintFontSize 将所有 maxHeight: 240ext.sentenceImageMaxHeight 将所有 alpha 硬编码 → 对应语义令牌

  • Step 3: 替换 sentence_detail_actions.dart 中的硬编码值

alpha: 0.12ext.sentenceActiveAlphaalpha: 0.4ext.sentenceActiveBorderAlphaalpha: 0.08ext.sentenceDangerAlpha

  • Step 4: 运行 Dart analyze 验证

Task 16: 更新 CHANGELOG + 最终验证

Files:

  • Modify: CHANGELOG.md

  • Step 1: 更新 CHANGELOG.md

记录所有变更到新版本号。

  • Step 2: 全量 Dart analyze

Run: dart analyze lib/ Expected: No issues found (or only pre-existing issues)

  • Step 3: 提交代码

执行顺序说明

Tasks 1-5 是基础设施提取(无功能变更),必须先完成。 Task 6 依赖 Tasks 1-5Provider中使用通用工具。 Tasks 7-9 是 Bug 修复和体验改善,依赖 Task 6。 Tasks 10-11 是新功能,依赖 Task 6。 Tasks 12-15 是润色优化,可并行。 Task 16 是收尾。