本次更新包含: 1. 更新应用标语与隐私政策文案,调整品牌宣传语 2. 重构Feed ID解析、HTML清理工具类,提取重复逻辑 3. 新增全屏图片查看器、通用动画操作按钮组件 4. 修复电池监听空指针、快捷操作异常捕获问题 5. 优化搜索、会话列表、RSS阅读器等页面体验 6. 完善多语言支持,新增多个翻译模块 7. 移除冗余代码,统一数字格式化逻辑 8. 调整登录页面布局与交互逻辑
27 KiB
句子详情面板全面重构 实施计划
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.stripTags 和 HtmlUtils.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.dart 的 HomeSentence 类中:
-
增加字段:
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
将 FullScreenPhotoView 从 sentence_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)读取detailItem和loadingDetail -
删除构造函数中的
detailItem和loadingDetail参数 -
保留
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) 读取 relatedItems 和 loadingRelated。每个推荐项显示: 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: 添加入场动画
使用 SingleTickerProviderStateMixin 或 ImplicitlyAnimatedWidget 为内容区域各 section 添加交错入场动画:
// 在 SentenceDetailContent 中使用 TweenAnimationBuilder
// 每个section延迟50ms出现,从下方淡入
将 SentenceDetailContent 从 StatelessWidget 改为 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: 48 → ext.sentenceQuoteFontSize
将所有 fontSize: 9 → ext.sentenceHintFontSize
将所有 maxHeight: 240 → ext.sentenceImageMaxHeight
将所有 alpha 硬编码 → 对应语义令牌
- Step 3: 替换 sentence_detail_actions.dart 中的硬编码值
将 alpha: 0.12 → ext.sentenceActiveAlpha
将 alpha: 0.4 → ext.sentenceActiveBorderAlpha
将 alpha: 0.08 → ext.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-5(Provider中使用通用工具)。 Tasks 7-9 是 Bug 修复和体验改善,依赖 Task 6。 Tasks 10-11 是新功能,依赖 Task 6。 Tasks 12-15 是润色优化,可并行。 Task 16 是收尾。