refactor: 重构项目目录结构与路径引用
1. 调整工具类、平台相关代码的目录组织,将原有根目录下的工具类迁移到`data/`和`platform/`子目录 2. 统一修复全项目的文件导入路径,匹配新的目录结构 3. 新增Web端平台适配的Stub实现,包括Isolate、path_provider、platform_io等 4. 删除旧的单文件平台适配实现,替换为分平台的目录结构实现 5. 移除旧的iOS Widget入口文件,新增Widget Extension的权限配置 6. 调整部分组件的目录位置,统一widget的分类组织 7. 修复部分硬编码文本和废弃的正则表达式逻辑
This commit is contained in:
793
docs/superpowers/plans/2026-05-23-bugfix-enhancement-plan.md
Normal file
793
docs/superpowers/plans/2026-05-23-bugfix-enhancement-plan.md
Normal file
@@ -0,0 +1,793 @@
|
||||
# 闲言APP Bug修复与功能增强 实施计划
|
||||
|
||||
> **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:** 修复5个关键Bug + 实现9项功能增强 + 2个目录重构
|
||||
|
||||
**Architecture:** 按优先级分4批执行:P0紧急Bug修复 → P1功能增强 → P2交互优化 → P3目录重构
|
||||
|
||||
**Tech Stack:** Flutter/Dart, Riverpod, Cupertino, confetti, flutter_animate, fl_chart, flutter_quill
|
||||
|
||||
---
|
||||
|
||||
## 批次一:P0 紧急Bug修复(Task 1-5)
|
||||
|
||||
### Task 1: 修复国学经典频道ID映射错误
|
||||
|
||||
**Files:**
|
||||
- Modify: `lib/features/classics/services/classics_service.dart:16-28`
|
||||
- Modify: `lib/features/tool_center/health/services/health_service.dart:16-28`
|
||||
|
||||
**问题:** 客户端硬编码的频道ID与后端 `$feedMap` key 不匹配,导致API报"不支持的频道"错误。
|
||||
|
||||
**后端正确的key映射表:**
|
||||
|
||||
| 分类名 | 客户端当前ID | 后端正确ID |
|
||||
|--------|------------|-----------|
|
||||
| 黄帝内经 | `huangdi` | `hdnj` |
|
||||
| 史记 | `shiji` | `sj` |
|
||||
| 孙子兵法 | `sunzi` | `sbbf` |
|
||||
| 墨子 | `mozi` | `mz` |
|
||||
| 庄子 | `zhuangzi` | `zz` |
|
||||
| 三国志 | `sanguozhi` | `sgz` |
|
||||
| 金匮要略 | `jingui` | `jgj` |
|
||||
| 战国策 | `zhanguoce` | `warring` |
|
||||
| 万历野获编 | `wanli` | 无对应(需后端新增或移除) |
|
||||
| 药茶养生 | `tea` | `tisana` |
|
||||
| 疾病自查 | `disease` | `illness` |
|
||||
|
||||
- [ ] **Step 1: 修复 classics_service.dart 中的ID映射**
|
||||
|
||||
将 `ClassicsCategory` 的 `id` 字段改为与后端一致:
|
||||
|
||||
```dart
|
||||
static const List<ClassicsCategory> categories = [
|
||||
ClassicsCategory(id: 'lunyu', name: '论语', emoji: '📖', desc: '孔子言行录'),
|
||||
ClassicsCategory(id: 'hdnj', name: '黄帝内经', emoji: '🏥', desc: '中医经典'),
|
||||
ClassicsCategory(id: 'sj', name: '史记', emoji: '📜', desc: '司马迁著'),
|
||||
ClassicsCategory(id: 'sbbf', name: '孙子兵法', emoji: '⚔️', desc: '兵家圣典'),
|
||||
ClassicsCategory(id: 'mz', name: '墨子', emoji: '🖤', desc: '墨家经典'),
|
||||
ClassicsCategory(id: 'zz', name: '庄子', emoji: '🦋', desc: '道家经典'),
|
||||
ClassicsCategory(id: 'zuozhuan', name: '左传', emoji: '📚', desc: '春秋左氏传'),
|
||||
ClassicsCategory(id: 'sgz', name: '三国志', emoji: '🏯', desc: '陈寿著'),
|
||||
ClassicsCategory(id: 'jgj', name: '金匮要略', emoji: '💊', desc: '张仲景著'),
|
||||
ClassicsCategory(id: 'warring', name: '战国策', emoji: '🗡️', desc: '纵横家书'),
|
||||
];
|
||||
```
|
||||
|
||||
移除 `wanli`(万历野获编),因为后端无对应频道。
|
||||
|
||||
- [ ] **Step 2: 修复 health_service.dart 中的ID映射**
|
||||
|
||||
```dart
|
||||
static const List<HealthCategory> categories = [
|
||||
HealthCategory(id: 'drug', name: '药品查询', emoji: '💊', desc: '药品信息·用法用量'),
|
||||
HealthCategory(id: 'herbal', name: '中草药', emoji: '🌿', desc: '中草药百科'),
|
||||
HealthCategory(id: 'food', name: '食物相克', emoji: '🍎', desc: '食物搭配禁忌'),
|
||||
HealthCategory(id: 'prescription', name: '偏方大全', emoji: '📋', desc: '民间偏方验方'),
|
||||
HealthCategory(id: 'tisana', name: '药茶养生', emoji: '🍵', desc: '养生药茶配方'),
|
||||
HealthCategory(id: 'illness', name: '疾病自查', emoji: '🏥', desc: '症状·疾病查询'),
|
||||
];
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 搜索所有引用旧ID的代码并更新**
|
||||
|
||||
搜索 `huangdi|shiji|sunzi|mozi|zhuangzi|sanguozhi|jingui|zhanguoce|wanli` 和 `tea|disease` 在整个 `lib/` 目录中的引用,确保没有硬编码的旧ID。
|
||||
|
||||
- [ ] **Step 4: 运行 flutter analyze 验证**
|
||||
|
||||
Run: `flutter analyze lib/features/classics/ lib/features/tool_center/health/`
|
||||
Expected: No issues found
|
||||
|
||||
---
|
||||
|
||||
### Task 2: 修复离线模式预加载频道使用中文名
|
||||
|
||||
**Files:**
|
||||
- Modify: `lib/core/storage/cache_config.dart:28,71-74`
|
||||
|
||||
**问题:** `preloadChannels` 默认值 `{'推荐', '经典', '诗词'}` 是中文,但后端 Feed API 的 channel 参数需要英文 key。
|
||||
|
||||
- [ ] **Step 1: 修改 CacheConfig 默认值**
|
||||
|
||||
```dart
|
||||
// 将中文频道名改为英文key
|
||||
this.preloadChannels = const {'all', 'poetry', 'wisdom'},
|
||||
```
|
||||
|
||||
同步修改 fromJson 中的默认值:
|
||||
|
||||
```dart
|
||||
preloadChannels: (json['preloadChannels'] as List<dynamic>?)
|
||||
?.map((e) => e.toString())
|
||||
.toSet() ??
|
||||
const {'all', 'poetry', 'wisdom'},
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 修改 offline_page.dart 中频道显示**
|
||||
|
||||
在离线模式页面的"预加载频道"行中,将英文key映射为中文名显示:
|
||||
|
||||
```dart
|
||||
_SettingRow(
|
||||
icon: CupertinoIcons.cube_box,
|
||||
title: '预加载频道',
|
||||
trailing: Text(config.preloadChannels.map(_channelDisplayName).join('、')),
|
||||
),
|
||||
```
|
||||
|
||||
添加映射方法:
|
||||
|
||||
```dart
|
||||
String _channelDisplayName(String key) {
|
||||
const map = {
|
||||
'all': '推荐',
|
||||
'poetry': '诗词',
|
||||
'wisdom': '哲理',
|
||||
'story': '故事',
|
||||
'hitokoto': '一言',
|
||||
'lyric': '歌词',
|
||||
'saying': '名言',
|
||||
};
|
||||
return map[key] ?? key;
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 运行 flutter analyze 验证**
|
||||
|
||||
---
|
||||
|
||||
### Task 3: 合并两套独立的预加载开关
|
||||
|
||||
**Files:**
|
||||
- Modify: `lib/mine/settings/providers/sub/performance_settings_provider.dart`
|
||||
- Modify: `lib/mine/settings/providers/general_settings_provider.dart`
|
||||
- Modify: `lib/mine/settings/presentation/general/general_settings_sections.dart`
|
||||
- Modify: `lib/features/home/presentation/providers/offline_page.dart`
|
||||
- Modify: `lib/features/home/services/offline_manager.dart`
|
||||
|
||||
**问题:** 通用设置中的 `preloadEnabled`(KV key: `general_preload`)和离线模式中的 `preloadOnWifi`(CacheConfig)互不关联。
|
||||
|
||||
**方案:** 统一使用 `CacheConfig.preloadOnWifi` 作为唯一预加载开关,移除 `PerformanceSettingsState.preloadEnabled`。
|
||||
|
||||
- [ ] **Step 1: 从 PerformanceSettingsState 移除 preloadEnabled**
|
||||
|
||||
移除 `preloadEnabled` 字段、`fromStorage` 中的读取、`saveToStorage` 中的保存、`setPreloadEnabled` 方法。
|
||||
|
||||
- [ ] **Step 2: 从 GeneralSettingsState 移除 preloadEnabled 透传**
|
||||
|
||||
移除 `bool get preloadEnabled` 和 `setPreloadEnabled` 方法。
|
||||
|
||||
- [ ] **Step 3: 修改通用设置页面**
|
||||
|
||||
将"预加载"开关改为读取/写入 `CacheConfig.preloadOnWifi`(通过 `offlineProvider`),而非 `generalSettingsProvider`。
|
||||
|
||||
- [ ] **Step 4: 修改 OfflineManager.preloadNow()**
|
||||
|
||||
在 `preloadNow()` 开头检查 `CacheConfig.preloadOnWifi`,如果关闭则返回 `PreloadResult(loaded: 0, skipped: 0)` 并在调用方提示"预加载已关闭"。
|
||||
|
||||
- [ ] **Step 5: 运行 flutter analyze 验证**
|
||||
|
||||
---
|
||||
|
||||
### Task 4: 增强阅读报告错误提示
|
||||
|
||||
**Files:**
|
||||
- Modify: `lib/features/reading_report/presentation/reading_report_page.dart`
|
||||
- Modify: `lib/features/reading_report/providers/reading_report_provider.dart`
|
||||
|
||||
**问题:** 当 dashboard API 返回空或格式变化时,仅显示"报告加载失败",用户无法理解原因。
|
||||
|
||||
- [ ] **Step 1: 修改 ReadingReportState 增加部分加载状态**
|
||||
|
||||
```dart
|
||||
class ReadingReportState {
|
||||
final ReadingReport? report;
|
||||
final ReportPeriod currentPeriod;
|
||||
final bool isLoading;
|
||||
final String? error;
|
||||
final Set<String> failedSources; // 新增:记录哪些数据源加载失败
|
||||
|
||||
// copyWith 也需更新
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 修改 ReadingReportService.generateReport 返回失败源信息**
|
||||
|
||||
在 `generateReport` 中,catch 块记录失败的数据源名称:
|
||||
|
||||
```dart
|
||||
final failedSources = <String>{};
|
||||
try { dashboard = await UserCenterService.getDashboard(); } catch (e) { failedSources.add('仪表盘'); }
|
||||
try { statsData = await UserCenterService.getStats(...); } catch (e) { failedSources.add('趋势数据'); }
|
||||
try { heatmapData = await UserCenterService.getHeatmap(); } catch (e) { failedSources.add('热力图'); }
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 修改阅读报告页面错误/部分加载提示**
|
||||
|
||||
当 `failedSources` 非空但 report 不为空时,在页面顶部显示警告条:
|
||||
|
||||
```dart
|
||||
if (state.failedSources.isNotEmpty && state.report != null)
|
||||
Container(
|
||||
padding: const EdgeInsets.all(AppSpacing.sm),
|
||||
decoration: BoxDecoration(color: CupertinoColors.systemYellow.withValues(alpha: 0.1), ...),
|
||||
child: Row(children: [
|
||||
Icon(CupertinoIcons.exclamationmark_triangle, color: CupertinoColors.systemYellow, size: 16),
|
||||
Text('部分数据加载失败:${state.failedSources.join('、')}', ...),
|
||||
]),
|
||||
),
|
||||
```
|
||||
|
||||
当全部失败时,显示更友好的错误信息:
|
||||
|
||||
```dart
|
||||
Widget _buildError(String error, AppThemeExtension ext) {
|
||||
return Center(child: Column(children: [
|
||||
Icon(CupertinoIcons.cloud_slash, size: 48, color: ext.textHint),
|
||||
Text('数据源暂时不可用', ...),
|
||||
Text('请检查网络连接后重试', ...),
|
||||
CupertinoButton(onPressed: retry, child: Text('重新加载')),
|
||||
]));
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: 运行 flutter analyze 验证**
|
||||
|
||||
---
|
||||
|
||||
### Task 5: 排行榜赛季自动创建(后端定时任务)
|
||||
|
||||
**Files:**
|
||||
- Create: `Scripts/create_rank_season.py` — Python脚本,通过API创建赛季
|
||||
- Modify: `docs/toolsapi/application/api/controller/Rank.php` — 添加自动创建当前赛季逻辑
|
||||
|
||||
**问题:** `rank_season` 表中没有 `status=active` 的记录,排行榜为空。
|
||||
|
||||
- [ ] **Step 1: 修改 Rank.php leaderboard 方法,自动创建当前周赛**
|
||||
|
||||
在 `leaderboard()` 方法中,当找不到活跃赛季时,自动创建:
|
||||
|
||||
```php
|
||||
if (!$currentSeason) {
|
||||
// 自动创建当前周赛
|
||||
$now = time();
|
||||
$weekStart = strtotime('monday this week', $now);
|
||||
$weekEnd = $weekStart + 7 * 86400;
|
||||
$weekNum = date('W', $now);
|
||||
$year = date('Y', $now);
|
||||
Db::name('rank_season')->insert([
|
||||
'name' => "{$year}年第{$weekNum}周赛",
|
||||
'type' => 'weekly',
|
||||
'start_time' => $weekStart,
|
||||
'end_time' => $weekEnd,
|
||||
'status' => 'active',
|
||||
'rewards' => json_encode([
|
||||
['rank_start' => 1, 'rank_end' => 3, 'exp' => 100, 'score' => 50],
|
||||
['rank_start' => 4, 'rank_end' => 10, 'exp' => 50, 'score' => 20],
|
||||
['rank_start' => 11, 'rank_end' => 50, 'exp' => 20, 'score' => 10],
|
||||
]),
|
||||
'createtime' => $now,
|
||||
'updatetime' => $now,
|
||||
]);
|
||||
$currentSeason = Db::name('rank_season')
|
||||
->where('status', 'active')
|
||||
->where('start_time', '<=', $now)
|
||||
->where('end_time', '>', $now)
|
||||
->order('start_time desc')
|
||||
->find();
|
||||
}
|
||||
```
|
||||
|
||||
同样在 `myRank()` 方法中添加相同的自动创建逻辑。
|
||||
|
||||
- [ ] **Step 2: 上传修改后的 Rank.php 到服务器**
|
||||
|
||||
使用 `Scripts/upload_server_code.py` 上传。
|
||||
|
||||
- [ ] **Step 3: 运行测试脚本验证**
|
||||
|
||||
Run: `python Scripts/test_rank_api.py`
|
||||
Expected: 排行榜API返回 `season != null`
|
||||
|
||||
---
|
||||
|
||||
## 批次二:P1 功能增强(Task 6-8)
|
||||
|
||||
### Task 6: 排行榜前3名动画效果
|
||||
|
||||
**Files:**
|
||||
- Modify: `lib/shared/widgets/rank_item_card.dart`
|
||||
- Modify: `lib/features/rank/presentation/rank_page.dart`
|
||||
|
||||
- [ ] **Step 1: 为 RankItemCard 添加 flutter_animate 动画**
|
||||
|
||||
前三名添加缩放+光晕入场动画:
|
||||
|
||||
```dart
|
||||
Widget build(BuildContext context) {
|
||||
final widget = Container(/* 现有卡片内容 */);
|
||||
|
||||
if (item.rank <= 3) {
|
||||
return widget
|
||||
.animate()
|
||||
.fadeIn(duration: 400.ms, delay: (item.rank * 100).ms)
|
||||
.scale(begin: const Offset(0.9, 0.9), end: const Offset(1, 1), duration: 400.ms)
|
||||
.shimmer(duration: 1200.ms, color: _rankColor.withValues(alpha: 0.15));
|
||||
}
|
||||
return widget.animate().fadeIn(duration: 300.ms, delay: (item.rank * 50).ms);
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 为第1名添加持续光晕效果**
|
||||
|
||||
```dart
|
||||
if (item.rank == 1) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: AppRadius.mdBorder,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: CupertinoColors.systemYellow.withValues(alpha: 0.3),
|
||||
blurRadius: 12,
|
||||
spreadRadius: 2,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: widget,
|
||||
).animate(onPlay: (c) => c.repeat()).shimmer(duration: 2000.ms, color: CupertinoColors.systemYellow.withValues(alpha: 0.1));
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 运行 flutter analyze 验证**
|
||||
|
||||
---
|
||||
|
||||
### Task 7: 阅读报告趋势图交互
|
||||
|
||||
**Files:**
|
||||
- Modify: `lib/features/reading_report/presentation/widgets/trend_chart.dart`
|
||||
|
||||
- [ ] **Step 1: 为 fl_chart LineChart 添加 touchCallback**
|
||||
|
||||
```dart
|
||||
LineChartData(
|
||||
lineTouchData: LineTouchData(
|
||||
touchTooltipData: LineTouchTooltipData(
|
||||
tooltipBgColor: ext.bgSecondary,
|
||||
tooltipRoundedRadius: 8,
|
||||
getTooltipItems: (spots) => spots.map((spot) {
|
||||
final date = trendData.length > spot.x.toInt()
|
||||
? trendData[spot.x.toInt()].date
|
||||
: '';
|
||||
return LineTooltipItem(
|
||||
'$date\n${spot.y.toInt()}',
|
||||
AppTypography.caption1.copyWith(color: ext.textPrimary),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
handleBuiltInTouches: true,
|
||||
),
|
||||
// ... 现有配置
|
||||
);
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 运行 flutter analyze 验证**
|
||||
|
||||
---
|
||||
|
||||
### Task 8: 排行榜领奖/成就解锁庆祝动画
|
||||
|
||||
**Files:**
|
||||
- Modify: `lib/features/rank/presentation/rank_page.dart`
|
||||
|
||||
- [ ] **Step 1: 为 RankPage 添加 ConfettiController**
|
||||
|
||||
```dart
|
||||
class _RankPageState extends ConsumerState<RankPage> {
|
||||
late ConfettiController _confettiController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_confettiController = ConfettiController(duration: const Duration(seconds: 2));
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_confettiController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 在 Stack 中添加 ConfettiWidget**
|
||||
|
||||
```dart
|
||||
Stack(children: [
|
||||
// 现有页面内容
|
||||
Align(
|
||||
alignment: Alignment.topCenter,
|
||||
child: ConfettiWidget(
|
||||
confettiController: _confettiController,
|
||||
blastDirectionality: BlastDirectionality.explosive,
|
||||
emissionFrequency: 0.05,
|
||||
numberOfParticles: 20,
|
||||
colors: [ext.accent, CupertinoColors.systemYellow, CupertinoColors.systemGreen],
|
||||
),
|
||||
),
|
||||
]);
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 领取奖励成功时触发动画**
|
||||
|
||||
在 `claimReward` 成功回调中调用 `_confettiController.play()`。
|
||||
|
||||
- [ ] **Step 4: 运行 flutter analyze 验证**
|
||||
|
||||
---
|
||||
|
||||
## 批次三:P2 交互优化(Task 9-12)
|
||||
|
||||
### Task 9: 笔记编辑器Markdown工具栏
|
||||
|
||||
**Files:**
|
||||
- Modify: `lib/features/note/presentation/note_edit_page.dart`
|
||||
|
||||
- [ ] **Step 1: 在编辑区域上方添加Markdown快捷工具栏**
|
||||
|
||||
在标题输入框和内容输入框之间,添加一行工具栏:
|
||||
|
||||
```dart
|
||||
Widget _buildMarkdownToolbar(AppThemeExtension ext) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: AppSpacing.sm, vertical: 4),
|
||||
decoration: BoxDecoration(color: ext.bgSecondary, borderRadius: AppRadius.smBorder),
|
||||
child: Wrap(
|
||||
spacing: 2,
|
||||
runSpacing: 2,
|
||||
children: [
|
||||
_toolbarBtn(ext, CupertinoIcons.bold, '粗体', () => _insertMarkdown('**', '**')),
|
||||
_toolbarBtn(ext, CupertinoIcons.italic, '斜体', () => _insertMarkdown('*', '*')),
|
||||
_toolbarBtn(ext, CupertinoIcons.list_bullet, '列表', () => _insertMarkdown('\n- ', '')),
|
||||
_toolbarBtn(ext, CupertinoIcons.number, '编号', () => _insertMarkdown('\n1. ', '')),
|
||||
_toolbarBtn(ext, CupertinoIcons.quote_closing, '引用', () => _insertMarkdown('\n> ', '')),
|
||||
_toolbarBtn(ext, CupertinoIcons.link, '链接', () => _insertMarkdown('[', '](url)')),
|
||||
_toolbarBtn(ext, CupertinoIcons.code, '代码', () => _insertMarkdown('`', '`')),
|
||||
_toolbarBtn(ext, CupertinoIcons.text_justify, '分割线', () => _insertMarkdown('\n---\n', '')),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _toolbarBtn(AppThemeExtension ext, IconData icon, String tooltip, VoidCallback onTap) {
|
||||
return CupertinoButton(
|
||||
padding: const EdgeInsets.all(6),
|
||||
minSize: 32,
|
||||
onPressed: onTap,
|
||||
child: Icon(icon, size: 18, color: ext.textSecondary),
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 实现 _insertMarkdown 方法**
|
||||
|
||||
在光标位置前后插入 Markdown 标记:
|
||||
|
||||
```dart
|
||||
void _insertMarkdown(String before, String after) {
|
||||
final controller = _contentController;
|
||||
final sel = controller.selection;
|
||||
final text = controller.text;
|
||||
final selected = sel.textInside(text);
|
||||
final newText = text.replaceRange(sel.start, sel.end, '$before$selected$after');
|
||||
controller.text = newText;
|
||||
controller.selection = TextSelection.collapsed(
|
||||
offset: sel.start + before.length + selected.length,
|
||||
);
|
||||
setState(() => _hasUnsavedChanges = true);
|
||||
_debounceSave();
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 运行 flutter analyze 验证**
|
||||
|
||||
---
|
||||
|
||||
### Task 10: SafeCachedImage 增加预加载和渐进式加载
|
||||
|
||||
**Files:**
|
||||
- Modify: `lib/shared/widgets/safe_cached_image.dart`
|
||||
|
||||
- [ ] **Step 1: 添加预加载静态方法**
|
||||
|
||||
```dart
|
||||
static Future<void> preload(String url) async {
|
||||
if (!_isValidUrl(url)) return;
|
||||
try {
|
||||
await CachedNetworkImage.evictFromCache(url);
|
||||
} catch (_) {}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 添加渐进式加载占位符**
|
||||
|
||||
在 `CachedNetworkImage` 的 `progressIndicatorBuilder` 中显示下载进度:
|
||||
|
||||
```dart
|
||||
progressIndicatorBuilder: (context, url, progress) {
|
||||
if (progress.downloaded != null && progress.totalSize != null) {
|
||||
final percent = (progress.downloaded! / progress.totalSize! * 100).toInt();
|
||||
return CupertinoActivityIndicator.partiallyRevealed(progress: percent / 100);
|
||||
}
|
||||
return const CupertinoActivityIndicator();
|
||||
},
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 运行 flutter analyze 验证**
|
||||
|
||||
---
|
||||
|
||||
### Task 11: 离线模式操作队列持久化
|
||||
|
||||
**Files:**
|
||||
- Modify: `lib/features/home/services/offline_manager.dart`
|
||||
- Modify: `lib/features/home/providers/offline_provider.dart`
|
||||
|
||||
- [ ] **Step 1: 在 OfflineManager.init() 中从 Hive 加载待执行操作**
|
||||
|
||||
```dart
|
||||
static Future<void> init() async {
|
||||
// ... 现有逻辑
|
||||
await _loadPendingActions();
|
||||
}
|
||||
|
||||
static Future<void> _loadPendingActions() async {
|
||||
final box = await Hive.openBox('offlineQueue');
|
||||
final saved = box.get('pendingActions', defaultValue: []) as List;
|
||||
for (final item in saved) {
|
||||
if (item is Map) {
|
||||
_pendingActions.add(OfflineAction.fromMap(Map<String, dynamic>.from(item)));
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 在操作入队时持久化到 Hive**
|
||||
|
||||
```dart
|
||||
static Future<void> enqueueAction(OfflineAction action) async {
|
||||
_pendingActions.add(action);
|
||||
final box = await Hive.openBox('offlineQueue');
|
||||
await box.put('pendingActions', _pendingActions.map((a) => a.toMap()).toList());
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 在操作完成后从 Hive 移除**
|
||||
|
||||
```dart
|
||||
static Future<void> _removeAction(int index) async {
|
||||
_pendingActions.removeAt(index);
|
||||
final box = await Hive.openBox('offlineQueue');
|
||||
await box.put('pendingActions', _pendingActions.map((a) => a.toMap()).toList());
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: 定义 OfflineAction 模型**
|
||||
|
||||
```dart
|
||||
class OfflineAction {
|
||||
final String type; // 'like', 'favorite', 'comment', etc.
|
||||
final Map<String, dynamic> data;
|
||||
final int createdAt;
|
||||
|
||||
const OfflineAction({required this.type, required this.data, required this.createdAt});
|
||||
|
||||
Map<String, dynamic> toMap() => {'type': type, 'data': data, 'createdAt': createdAt};
|
||||
factory OfflineAction.fromMap(Map<String, dynamic> m) =>
|
||||
OfflineAction(type: m['type'], data: Map<String, dynamic>.from(m['data']), createdAt: m['createdAt']);
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 5: 运行 flutter analyze 验证**
|
||||
|
||||
---
|
||||
|
||||
### Task 12: 深度链接支持 + 鸿蒙端路由同步
|
||||
|
||||
**Files:**
|
||||
- Modify: `lib/core/router/app_routes.dart`
|
||||
- Modify: `lib/core/router/content_routes.dart`
|
||||
- Modify: `lib/core/router/ohos_nav_bridge.dart`
|
||||
- Modify: `lib/core/router/settings_routes.dart`
|
||||
|
||||
- [ ] **Step 1: 在 app_routes.dart 中添加深度链接路由常量**
|
||||
|
||||
```dart
|
||||
static const String deepArticle = '/article/:id';
|
||||
static const String deepFortune = '/fortune';
|
||||
static const String deepNote = '/notes';
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 在 GoRouter 中注册深度链接处理**
|
||||
|
||||
在 `content_routes.dart` 或 `app_router.dart` 中,添加 `redirect` 逻辑处理通用链接:
|
||||
|
||||
```dart
|
||||
redirect: (context, state) {
|
||||
// 处理通用链接: xianyan://article/123 → /article/123
|
||||
final uri = state.uri;
|
||||
if (uri.scheme == 'xianyan') {
|
||||
return uri.path;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 在 ohos_nav_bridge.dart 中同步新增路由**
|
||||
|
||||
确保所有新增路由都在 `_routes` 列表中有对应的 `OhosRouteEntry`:
|
||||
|
||||
```dart
|
||||
OhosRouteEntry(pattern: '/other-settings', builder: (_) => const OtherSettingsPage()),
|
||||
OhosRouteEntry(pattern: '/article/:id', builder: (ctx) => ArticleDetailPage(articleId: ctx.params.getInt('id'))),
|
||||
```
|
||||
|
||||
- [ ] **Step 4: 运行 flutter analyze 验证**
|
||||
|
||||
---
|
||||
|
||||
## 批次四:P3 目录重构(Task 13-14)
|
||||
|
||||
### Task 13: 重构 lib/shared/widgets/ 目录
|
||||
|
||||
**当前问题:** 36个文件平铺在一个目录下,难以维护。
|
||||
|
||||
**目标结构:**
|
||||
|
||||
```
|
||||
lib/shared/widgets/
|
||||
├── widgets.dart # 统一导出(保持不变)
|
||||
├── adaptive/ # 自适应组件
|
||||
│ ├── adaptive_back_button.dart
|
||||
│ └── responsive_layout.dart
|
||||
├── animation/ # 动画组件
|
||||
│ ├── animated_widgets.dart
|
||||
│ └── sprite_loading_indicator.dart
|
||||
├── containers/ # 容器组件
|
||||
│ ├── glass_container.dart
|
||||
│ ├── glass_bottom_nav_bar.dart
|
||||
│ └── shader_card_background.dart
|
||||
├── feedback/ # 反馈组件
|
||||
│ ├── app_toast.dart
|
||||
│ ├── empty_state.dart
|
||||
│ ├── skeleton.dart
|
||||
│ ├── app_error_boundary.dart
|
||||
│ └── offline_banner.dart
|
||||
├── input/ # 输入组件
|
||||
│ ├── bottom_sheet.dart
|
||||
│ ├── keyboard_back_handler.dart
|
||||
│ ├── keyboard_safe_sheet.dart
|
||||
│ └── app_popup_menu.dart
|
||||
├── media/ # 媒体组件
|
||||
│ ├── safe_cached_image.dart
|
||||
│ ├── tts_player_bar.dart
|
||||
│ └── wallpaper_gallery/ # 保持子目录不变
|
||||
├── navigation/ # 导航组件
|
||||
│ ├── appbar_character_sprite.dart
|
||||
│ ├── appbar_date_display.dart
|
||||
│ ├── tab_icon_sprite.dart
|
||||
│ └── app_page_transitions.dart
|
||||
├── content/ # 内容展示组件
|
||||
│ ├── app_markdown.dart
|
||||
│ ├── app_slidable.dart
|
||||
│ ├── app_sticky_header.dart
|
||||
│ ├── category_icon.dart
|
||||
│ ├── level_card.dart
|
||||
│ ├── rank_item_card.dart
|
||||
│ ├── task_card.dart
|
||||
│ ├── character_tip_bubble.dart
|
||||
│ ├── share_sheet.dart
|
||||
│ └── app_icon.dart
|
||||
└── layout/ # 布局组件
|
||||
└── (reserved for future)
|
||||
```
|
||||
|
||||
- [ ] **Step 1: 创建子目录并移动文件**
|
||||
|
||||
按上述结构移动文件。每个文件移动后,更新内部 import 路径。
|
||||
|
||||
- [ ] **Step 2: 更新 widgets.dart 统一导出文件**
|
||||
|
||||
```dart
|
||||
export 'adaptive/adaptive_back_button.dart';
|
||||
export 'adaptive/responsive_layout.dart';
|
||||
export 'animation/animated_widgets.dart';
|
||||
export 'animation/sprite_loading_indicator.dart';
|
||||
export 'containers/glass_container.dart';
|
||||
export 'containers/glass_bottom_nav_bar.dart';
|
||||
export 'containers/shader_card_background.dart';
|
||||
export 'feedback/app_toast.dart';
|
||||
export 'feedback/empty_state.dart';
|
||||
export 'feedback/skeleton.dart';
|
||||
export 'feedback/app_error_boundary.dart';
|
||||
export 'feedback/offline_banner.dart';
|
||||
export 'input/bottom_sheet.dart';
|
||||
export 'input/keyboard_back_handler.dart';
|
||||
export 'input/keyboard_safe_sheet.dart';
|
||||
export 'input/app_popup_menu.dart';
|
||||
export 'media/safe_cached_image.dart';
|
||||
export 'media/tts_player_bar.dart';
|
||||
export 'navigation/appbar_character_sprite.dart';
|
||||
export 'navigation/appbar_date_display.dart';
|
||||
export 'navigation/tab_icon_sprite.dart';
|
||||
export 'navigation/app_page_transitions.dart';
|
||||
export 'content/app_markdown.dart';
|
||||
export 'content/app_slidable.dart';
|
||||
export 'content/app_sticky_header.dart';
|
||||
export 'content/category_icon.dart';
|
||||
export 'content/level_card.dart';
|
||||
export 'content/rank_item_card.dart';
|
||||
export 'content/task_card.dart';
|
||||
export 'content/character_tip_bubble.dart';
|
||||
export 'content/share_sheet.dart';
|
||||
export 'content/app_icon.dart';
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 全局搜索并更新所有 import 路径**
|
||||
|
||||
搜索 `package:xianyan/shared/widgets/` 开头的 import,确保通过 `widgets.dart` 统一导出的路径仍然有效(因为 widgets.dart 重新导出了所有组件,大部分 import 不需要改)。
|
||||
|
||||
对于直接引用子文件的 import(如 `package:xianyan/shared/widgets/glass_container.dart`),更新为新路径。
|
||||
|
||||
- [ ] **Step 4: 运行 flutter analyze 验证**
|
||||
|
||||
---
|
||||
|
||||
### Task 14: 重构 lib/core/utils/ 目录
|
||||
|
||||
**当前问题:** 18个文件平铺在一个目录下。
|
||||
|
||||
**目标结构:**
|
||||
|
||||
```
|
||||
lib/core/utils/
|
||||
├── logger.dart # 日志(保持不变,被广泛引用)
|
||||
├── extensions.dart # 扩展方法(保持不变)
|
||||
├── pattern_utils.dart # 正则工具(保持不变)
|
||||
├── platform/ # 平台相关
|
||||
│ ├── platform_utils.dart
|
||||
│ ├── platform_helper.dart
|
||||
│ ├── platform_feature_guard.dart
|
||||
│ ├── platform_io_native.dart
|
||||
│ ├── platform_io_stub.dart
|
||||
│ ├── device_detection.dart
|
||||
│ └── path_provider_native.dart
|
||||
│ └── path_provider_stub.dart
|
||||
├── ui/ # UI工具
|
||||
│ ├── page_transitions.dart
|
||||
│ ├── interaction_animations.dart
|
||||
│ ├── sheet_animation_notifier.dart
|
||||
│ └── clipboard_bridge.dart
|
||||
├── data/ # 数据工具
|
||||
│ ├── level_utils.dart
|
||||
│ └── receipt_helper.dart
|
||||
└── isolate_stub.dart # isolate(保持不变)
|
||||
```
|
||||
|
||||
- [ ] **Step 1: 创建子目录并移动文件**
|
||||
|
||||
- [ ] **Step 2: 全局搜索并更新所有 import 路径**
|
||||
|
||||
- [ ] **Step 3: 运行 flutter analyze 验证**
|
||||
|
||||
---
|
||||
|
||||
## 验收标准
|
||||
|
||||
| 批次 | 验收项 |
|
||||
|------|--------|
|
||||
| P0 | `flutter analyze lib/` 零 error;国学经典/健康模块所有分类可正常加载;预加载使用英文key;只有一套预加载开关;阅读报告有友好错误提示;排行榜自动创建赛季 |
|
||||
| P1 | 排行榜前3名有缩放+光晕动画;趋势图可点击查看数据详情;领奖有庆祝动画 |
|
||||
| P2 | 笔记编辑器有Markdown工具栏;图片有渐进式加载;离线操作重启后不丢失;深度链接可跳转;鸿蒙端路由同步 |
|
||||
| P3 | widgets目录按职责分子目录;utils目录按职责分子目录;所有import路径正确 |
|
||||
Reference in New Issue
Block a user