chore: 汇总批量提交的功能优化与bug修复

本次提交包含多项迭代优化和问题修复:
1. 新增缩略图图片组件、数字格式化工具类,补充多语言翻译类型与本地化支持
2. 优化底部导航栏主题色统一使用动态accent色值
3. 修复多处图表动画、路由跳转、API请求相关问题
4. 简化服务器公告文案,调整默认分屏状态为关闭
5. 新增安卓/iOS桌面快捷方式配置
6. 重构多处状态管理类使用SafeNotifierInit统一异常保护
7. 替换硬编码蓝色为主题色,更新版本号获取方式为动态读取
8. 优化缓存预加载逻辑,移除无用代码
9. 调整默认设置项,优化用户体验细节
This commit is contained in:
Developer
2026-05-31 12:24:05 +08:00
parent 0da8906f5d
commit 9ea8d3d606
298 changed files with 48547 additions and 21836 deletions

View File

@@ -1,300 +0,0 @@
# 养成体系 v2.0 实施计划
> **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:** 实现EXP独立体系、等级展示、勋章系统、赛季排行榜、每日任务五大养成功能
**Architecture:** 服务端基于ThinkPHP5(FastAdmin)新增7张数据库表、2个API控制器、7个管理控制器客户端基于Flutter新增排行榜/任务/勋章墙3个功能模块修改用户中心/签到/成就等现有页面
**Tech Stack:** PHP(ThinkPHP5/FastAdmin) + MySQL + Flutter(Dart) + Python(测试脚本)
---
## Phase 1: EXP独立体系 + 等级展示
### Task 1.1: 数据库迁移
**Files:**
- Create: `docs/toolsapi/application/admin/command/Install/migrate_v11.sql`
- [ ] 创建迁移SQL: tool_user新增exp字段 + tool_user_exp_log表
### Task 1.2: 服务端ExpEngine核心
**Files:**
- Modify: `docs/toolsapi/application/common/model/User.php`
- [ ] User模型新增exp字段类型定义
- [ ] 新增nextlevelByExp()方法: floor(exp^0.4/10)+1, 10级封顶
- [ ] 新增exp()静态方法: 带事务锁的EXP增减自动计算level
- [ ] 修改score()方法: 积分变动时同时产出EXP
### Task 1.3: 服务端UserCenter API
**Files:**
- Modify: `docs/toolsapi/application/api/controller/UserCenter.php`
- [ ] index()返回新增level/exp/exp_to_next/exp_progress字段
- [ ] 新增expLog()方法: EXP变动日志分页查询
- [ ] signin()方法: 签到时同时产出EXP(5+continuous*2)
- [ ] checkin相关: 打卡时产出EXP(10)
### Task 1.4: 客户端数据模型
**Files:**
- Modify: `lib/features/auth/models/user_model.dart`
- Modify: `lib/features/user_center/models/user_center_models.dart`
- [ ] UserModel新增level/exp/expToNext/expProgress字段
- [ ] UserCenterInfo新增level/exp相关字段
### Task 1.5: 客户端等级卡片组件
**Files:**
- Create: `lib/shared/widgets/level_card.dart`
- [ ] 创建等级卡片组件: 等级数字+称号+EXP进度条+下一级所需
### Task 1.6: 客户端页面集成
**Files:**
- Modify: `lib/features/signin/presentation/signin_page.dart`
- Modify: `lib/features/achievement/presentation/achievement_page.dart`
- Modify: `lib/features/user_center/presentation/user_center_page.dart`
- [ ] 签到页: 签到卡片增加等级展示
- [ ] 成就页: 概要增加等级信息
- [ ] 用户中心: 增加等级卡片
### Task 1.7: 上传服务器+测试
- [ ] 上传PHP文件到服务器
- [ ] 执行数据库迁移
- [ ] 编写EXP全流程测试脚本
- [ ] 运行测试并验证
---
## Phase 2: 勋章系统
### Task 2.1: 数据库迁移
**Files:**
- Modify: `docs/toolsapi/application/admin/command/Install/migrate_v11.sql`
- [ ] 追加tool_badge + tool_user_badge建表SQL
- [ ] 追加20个预置勋章INSERT语句
### Task 2.2: 服务端BadgeEngine
**Files:**
- Modify: `docs/toolsapi/application/api/controller/Achievement.php`
- [ ] 新增badges()方法: 获取所有勋章列表(含用户解锁状态)
- [ ] 新增badgeDisplay()方法: 设置主页展示勋章(最多3个)
- [ ] 新增_checkBadges()方法: 勋章检测逻辑
- [ ] claim()方法: 领取成就后触发勋章检测
- [ ] signin()后触发勋章检测
### Task 2.3: 管理员后台-勋章管理
**Files:**
- Create: `docs/toolsapi/application/admin/controller/Badge.php`
- Create: `docs/toolsapi/application/admin/model/Badge.php`
- Create: `docs/toolsapi/application/admin/validate/Badge.php`
- Create: `docs/toolsapi/application/admin/view/badge/index.html`
- Create: `docs/toolsapi/application/admin/view/badge/add.html`
- Create: `docs/toolsapi/application/admin/view/badge/edit.html`
- Create: `docs/toolsapi/public/assets/js/backend/badge.js`
- Create: `docs/toolsapi/application/admin/lang/zh-cn/badge.php`
- [ ] 勋章管理CRUD(列表/添加/编辑/删除)
- [ ] 勋章图标/稀有度/条件配置
### Task 2.4: 管理员后台-用户勋章
**Files:**
- Create: `docs/toolsapi/application/admin/controller/user/UserBadge.php`
- Create: `docs/toolsapi/application/admin/model/UserBadge.php`
- Create: `docs/toolsapi/application/admin/view/user/user_badge/index.html`
- Create: `docs/toolsapi/public/assets/js/backend/user/user_badge.js`
- Create: `docs/toolsapi/application/admin/lang/zh-cn/user/user_badge.php`
- [ ] 用户勋章列表(查看/手动发放/收回)
### Task 2.5: 客户端勋章墙
**Files:**
- Create: `lib/features/achievement/presentation/badge_wall_page.dart`
- Create: `lib/features/achievement/providers/badge_provider.dart`
- Create: `lib/shared/widgets/badge_icon.dart`
- [ ] 勋章图标组件(含稀有度边框颜色)
- [ ] 勋章墙页面(全部勋章网格+已解锁/未解锁状态)
- [ ] 主页展示设置(选择最多3个展示)
### Task 2.6: 上传服务器+测试
- [ ] 上传PHP文件+执行迁移
- [ ] 编写勋章全流程测试脚本
- [ ] 运行测试并验证
---
## Phase 3: 每日任务
### Task 3.1: 数据库迁移
- [ ] 追加tool_daily_task + tool_user_task建表SQL
- [ ] 追加8个预置任务INSERT语句
### Task 3.2: 服务端TaskEngine
**Files:**
- Create: `docs/toolsapi/application/api/controller/Task.php`
- [ ] today()方法: 获取今日任务列表(含进度)
- [ ] reportProgress()方法: 上报任务进度
- [ ] claim()方法: 领取任务奖励
- [ ] claimPerfect()方法: 领取完美日奖励
- [ ] registerCustom()方法: 注册自定义任务
### Task 3.3: 管理员后台-任务管理
**Files:**
- Create: `docs/toolsapi/application/admin/controller/DailyTask.php` + 配套model/validate/view/js/lang
- Create: `docs/toolsapi/application/admin/controller/user/UserTask.php` + 配套
- [ ] 每日任务CRUD
- [ ] 用户任务进度查看
### Task 3.4: 客户端每日任务页
**Files:**
- Create: `lib/features/task/presentation/daily_task_page.dart`
- Create: `lib/features/task/providers/task_provider.dart`
- Create: `lib/features/task/services/task_service.dart`
- Create: `lib/shared/widgets/task_card.dart`
- [ ] 任务卡片组件
- [ ] 每日任务页面(任务列表+进度+领取)
- [ ] 完美日动画效果
### Task 3.5: 上传服务器+测试
- [ ] 上传+迁移+测试
---
## Phase 4: 赛季排行榜
### Task 4.1: 数据库迁移
- [ ] 追加tool_rank_season + tool_rank_record建表SQL
### Task 4.2: 服务端RankEngine
**Files:**
- Create: `docs/toolsapi/application/api/controller/Rank.php`
- [ ] seasons()方法: 赛季列表
- [ ] leaderboard()方法: 排行榜查询
- [ ] myRank()方法: 我的排名
- [ ] claimReward()方法: 领取赛季奖励
- [ ] 赛季自动创建+结算逻辑
### Task 4.3: 管理员后台-赛季管理
**Files:**
- Create: `docs/toolsapi/application/admin/controller/RankSeason.php` + 配套
- Create: `docs/toolsapi/application/admin/controller/RankRecord.php` + 配套
- [ ] 赛季CRUD+奖励配置+手动结算
- [ ] 排名记录查看+手动发放奖励
### Task 4.4: 客户端排行榜页
**Files:**
- Create: `lib/features/rank/presentation/rank_page.dart`
- Create: `lib/features/rank/providers/rank_provider.dart`
- Create: `lib/features/rank/services/rank_service.dart`
- Create: `lib/shared/widgets/rank_item.dart`
- [ ] 排行榜页面(周赛/月赛Tab + 4种排行类型)
- [ ] 排行条目组件(排名+头像+等级+数值)
- [ ] 我的排名卡片+奖励领取
### Task 4.5: 上传服务器+测试
- [ ] 上传+迁移+测试
---
## Phase 5: 管理员后台完善
### Task 5.1: 用户管理扩展
**Files:**
- Modify: `docs/toolsapi/application/admin/controller/user/User.php`
- Modify: `docs/toolsapi/application/admin/view/user/user/edit.html`
- [ ] edit()增加exp字段编辑
- [ ] edit.html增加exp输入框+level只读显示
### Task 5.2: EXP日志管理
**Files:**
- Create: `docs/toolsapi/application/admin/controller/user/ExpLog.php` + 配套
- [ ] EXP日志查看(只读列表)
### Task 5.3: 菜单权限配置
- [ ] fa_auth_rule INSERT: 养成管理一级菜单+7个二级菜单+权限节点
### Task 5.4: 上传服务器+验证
- [ ] 上传所有管理后台文件
- [ ] 执行菜单SQL
- [ ] 验证后台功能
---
## 归档列表
### Phase 1: EXP独立体系 + 等级展示
- [x] ✅ Task 1.1: 数据库迁移
- [x] ✅ Task 1.2: 服务端ExpEngine核心
- [x] ✅ Task 1.3: 服务端UserCenter API
- [x] ✅ Task 1.4: 客户端数据模型
- [x] ✅ Task 1.5: 客户端等级卡片组件
- [x] ✅ Task 1.6: 客户端页面集成
- [x] ✅ Task 1.7: 上传服务器+测试(7/7通过)
### Phase 2: 勋章系统
- [x] ✅ Task 2.1: 数据库迁移
- [x] ✅ Task 2.2: 服务端BadgeEngine
- [x] ✅ Task 2.3: 管理员后台-勋章管理
- [x] ✅ Task 2.4: 管理员后台-用户勋章
- [x] ✅ Task 2.5: 客户端勋章墙
- [x] ✅ Task 2.6: 上传服务器+测试(7/7通过)
### Phase 3: 每日任务
- [ ] Task 3.1: 数据库迁移
- [ ] Task 3.2: 服务端TaskEngine
- [ ] Task 3.3: 管理员后台-任务管理
- [ ] Task 3.4: 客户端每日任务页
- [ ] Task 3.5: 上传服务器+测试
### Phase 4: 赛季排行榜
- [ ] Task 4.1: 数据库迁移
- [ ] Task 4.2: 服务端RankEngine
- [ ] Task 4.3: 管理员后台-赛季管理
- [ ] Task 4.4: 客户端排行榜页
- [ ] Task 4.5: 上传服务器+测试
### Phase 5: 管理员后台完善
- [ ] Task 5.1: 用户管理扩展
- [ ] Task 5.2: EXP日志管理
- [ ] Task 5.3: 菜单权限配置
- [ ] Task 5.4: 上传服务器+验证

File diff suppressed because it is too large Load Diff

View File

@@ -1,709 +0,0 @@
# OhosNavBridge 注册表+中间件重构 实施计划
> **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:** 将 OhosNavBridge 从手动 if-else 路由映射重构为注册表驱动+中间件管道,实现自动参数解析、路由守卫、增强 replace/go 导航。
**Architecture:** 新增 `OhosRouteParams`/`OhosRouteContext`/`OhosMiddlewareResult`/`OhosNavMiddleware`/`OhosRouteEntry` 五个核心类,替代原有 `_routeMap` + 手动 if-else。路由注册表统一管理所有路由含参数路由中间件管道在导航执行前拦截/放行/重定向。`AuthMiddleware` 通过 Riverpod `authProvider` 检查登录态。
**Tech Stack:** Flutter/Dart, Riverpod (authProvider), CupertinoPageRoute, GoRouter (非鸿蒙端)
---
## File Structure
| 操作 | 文件路径 | 职责 |
|------|---------|------|
| 修改 | `lib/core/router/ohos_nav_bridge.dart` | 核心重构:注册表+中间件+自动参数解析 |
| 修改 | `lib/core/router/app_nav_extension.dart` | 无需改动,接口不变 |
| 修改 | `lib/core/layout/ohos_app_shell.dart` | 暴露 Tab 切换能力给 OhosNavBridge.go() |
---
### Task 1: 新增核心数据结构
**Files:**
- Modify: `lib/core/router/ohos_nav_bridge.dart`
- [ ] **Step 1: 在文件顶部OhosNavBridge 类之前)添加核心数据结构**
`import` 区域之后、`OhosNavBridge` 类之前,添加以下代码:
```dart
/// 路由参数解析结果 — 自动从 URL 提取路径参数和查询参数
class OhosRouteParams {
const OhosRouteParams({
Map<String, String>? pathParams,
Map<String, String>? queryParams,
}) : _pathParams = pathParams ?? const {},
_queryParams = queryParams ?? const {};
final Map<String, String> _pathParams;
final Map<String, String> _queryParams;
String getString(String key, [String defaultValue = '']) =>
_pathParams[key] ?? _queryParams[key] ?? defaultValue;
int getInt(String key, [int defaultValue = 0]) =>
int.tryParse(getString(key)) ?? defaultValue;
bool getBool(String key, [bool defaultValue = false]) =>
getString(key, defaultValue.toString()).toLowerCase() == 'true';
@override
String toString() =>
'OhosRouteParams(pathParams: $_pathParams, queryParams: $_queryParams)';
}
/// 路由上下文 — 传递给中间件和 builder 的完整信息
class OhosRouteContext {
const OhosRouteContext({
required this.path,
required this.params,
required this.navigatorContext,
this.extra,
});
final String path;
final OhosRouteParams params;
final Object? extra;
final BuildContext navigatorContext;
}
/// 中间件执行结果
sealed class OhosMiddlewareResult {
const OhosMiddlewareResult();
}
/// 放行 — 继续执行下一个中间件或构建页面
class OhosMiddlewareNext extends OhosMiddlewareResult {
const OhosMiddlewareNext();
}
/// 重定向 — 中断当前导航,跳转到新路由
class OhosMiddlewareRedirect extends OhosMiddlewareResult {
const OhosMiddlewareRedirect(this.path, {this.extra});
final String path;
final Object? extra;
}
/// 拒绝 — 中断当前导航,不跳转
class OhosMiddlewareReject extends OhosMiddlewareResult {
const OhosMiddlewareReject(this.reason);
final String reason;
}
/// 中间件基类 — 在路由跳转前执行拦截逻辑
abstract class OhosNavMiddleware {
OhosMiddlewareResult handle(OhosRouteContext context);
}
/// 路由注册条目 — 替代原来的 Map<String, WidgetBuilder>
class OhosRouteEntry {
const OhosRouteEntry({
required this.pattern,
required this.builder,
this.middlewares = const [],
});
/// 路由模式,如 '/article/:id' 或 '/settings/theme'
final String pattern;
/// 页面构建器,接收 OhosRouteContext 自动获取参数
final Widget Function(OhosRouteContext context) builder;
/// 中间件列表,按顺序执行
final List<OhosNavMiddleware> middlewares;
}
```
- [ ] **Step 2: 验证编译通过**
Run: `cd e:\project\flutter\f\xianyan && flutter analyze lib/core/router/ohos_nav_bridge.dart 2>&1 | head -20`
Expected: 无错误(可能有 unused warning正常
---
### Task 2: 添加 AuthMiddleware 和 LogMiddleware
**Files:**
- Modify: `lib/core/router/ohos_nav_bridge.dart`
- [ ] **Step 1: 添加 import 并实现 AuthMiddleware**
在文件顶部 import 区域添加:
```dart
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:xianyan/features/auth/providers/auth_provider.dart';
```
`OhosRouteEntry` 类之后、`OhosNavBridge` 类之前,添加:
```dart
/// 登录态拦截中间件 — 未登录时重定向到登录页
class AuthMiddleware extends OhosNavMiddleware {
@override
OhosMiddlewareResult handle(OhosRouteContext context) {
try {
final container = ProviderScope.containerOf(context.navigatorContext);
final isLoggedIn = container.read(authProvider).isLoggedIn;
if (!isLoggedIn) {
return const OhosMiddlewareRedirect('/login');
}
} catch (e) {
Log.w('🟡 [OHOS] AuthMiddleware: 无法读取登录状态: $e');
return const OhosMiddlewareRedirect('/login');
}
return const OhosMiddlewareNext();
}
}
/// 路由访问日志中间件 — 调试模式下记录导航信息
class LogMiddleware extends OhosNavMiddleware {
@override
OhosMiddlewareResult handle(OhosRouteContext context) {
Log.i(
'🟢 [OHOS] Navigate: ${context.path} '
'params=${context.params._pathParams} '
'query=${context.params._queryParams} '
'extra=${context.extra?.runtimeType}',
);
return const OhosMiddlewareNext();
}
}
```
- [ ] **Step 2: 验证编译通过**
Run: `cd e:\project\flutter\f\xianyan && flutter analyze lib/core/router/ohos_nav_bridge.dart 2>&1 | head -20`
Expected: 无错误
---
### Task 3: 重构 OhosNavBridge — 注册表 + 匹配算法 + push()
**Files:**
- Modify: `lib/core/router/ohos_nav_bridge.dart`
- [ ] **Step 1: 替换 `_routeMap` 为 `_routes` 注册表**
删除整个 `_routeMap` 定义(约 80 行),替换为 `_routes` 列表:
```dart
/// 路由注册表 — 统一管理所有路由(含参数路由和中间件)
static final List<OhosRouteEntry> _routes = [
// ---- 无参数路由 ----
OhosRouteEntry(pattern: '/search', builder: (_) => const SearchPage()),
OhosRouteEntry(pattern: '/favorites', builder: (_) => const FavoritePage()),
OhosRouteEntry(pattern: '/history', builder: (_) => const HistoryPage()),
OhosRouteEntry(pattern: '/likes', builder: (_) => const LikesPage()),
OhosRouteEntry(pattern: '/readlater', builder: (_) => const ReadLaterPage()),
OhosRouteEntry(pattern: '/offline', builder: (_) => const OfflinePage()),
OhosRouteEntry(pattern: '/cache', builder: (_) => const CacheManagementPage()),
OhosRouteEntry(pattern: '/footprint', builder: (_) => const FootprintPage()),
OhosRouteEntry(pattern: '/about', builder: (_) => const AboutPage()),
OhosRouteEntry(pattern: '/login', builder: (_) => const LoginPage()),
OhosRouteEntry(pattern: '/signin', builder: (_) => const SigninPage()),
OhosRouteEntry(pattern: '/my-devices', builder: (_) => const MyDevicesPage()),
OhosRouteEntry(pattern: '/qrcode-login', builder: (_) => const QrcodeLoginPage()),
OhosRouteEntry(pattern: '/notes', middlewares: [AuthMiddleware()], builder: (_) => const NoteListPage()),
OhosRouteEntry(pattern: '/notes/edit', middlewares: [AuthMiddleware()], builder: (_) => const NoteEditPage()),
OhosRouteEntry(pattern: '/statistics', builder: (_) => const StatisticsPage()),
OhosRouteEntry(pattern: '/correction', builder: (_) => const CorrectionPage()),
OhosRouteEntry(pattern: '/tool/ocr', builder: (_) => const OcrToolPage()),
OhosRouteEntry(pattern: '/tool/pinyin', builder: (_) => const PinyinToolPage()),
OhosRouteEntry(pattern: '/tool/china_colors', builder: (_) => const ChinaColorsPage()),
OhosRouteEntry(pattern: '/source', builder: (_) => const SourcePage()),
OhosRouteEntry(pattern: '/member', builder: (_) => const MemberPage()),
OhosRouteEntry(pattern: '/settings/theme', builder: (_) => const ThemeSettingsPage()),
OhosRouteEntry(pattern: '/settings/general', builder: (_) => const GeneralSettingsPage()),
OhosRouteEntry(pattern: '/settings/language', builder: (_) => const LanguageSettingsPage()),
OhosRouteEntry(pattern: '/settings/account', middlewares: [AuthMiddleware()], builder: (_) => const AccountSettingsPage()),
OhosRouteEntry(pattern: '/settings/account/deletion', middlewares: [AuthMiddleware()], builder: (_) => const AccountDeletionPage()),
OhosRouteEntry(pattern: '/settings/data', builder: (_) => const DataManagementPage()),
OhosRouteEntry(pattern: '/settings/password', middlewares: [AuthMiddleware()], builder: (_) => const ChangePasswordPage()),
OhosRouteEntry(pattern: '/settings/security-question', middlewares: [AuthMiddleware()], builder: (_) => const SecurityQuestionPage()),
OhosRouteEntry(pattern: '/settings/fonts', builder: (_) => const FontManagementPage()),
OhosRouteEntry(pattern: '/notification-settings', builder: (_) => const NotificationSettingsPage()),
OhosRouteEntry(pattern: '/smart-mode-settings', builder: (_) => const SmartModeSettingsPage()),
OhosRouteEntry(pattern: '/more-settings', builder: (_) => const MoreSettingsPage()),
OhosRouteEntry(pattern: '/permission-management', builder: (_) => const PermissionManagementPage()),
OhosRouteEntry(pattern: '/privacy-policy', builder: (_) => const PrivacyPolicyPage()),
OhosRouteEntry(pattern: '/log-viewer', builder: (_) => const LogViewerPage()),
OhosRouteEntry(pattern: '/discover', builder: (_) => const DiscoverPage()),
OhosRouteEntry(pattern: '/hot-search', builder: (_) => const HotSearchPage()),
OhosRouteEntry(pattern: '/user-preference', builder: (_) => const UserPreferencePage()),
OhosRouteEntry(pattern: '/learning', builder: (_) => const LearningCenterPage()),
OhosRouteEntry(pattern: '/learning-progress', builder: (_) => const LearningProgressPage()),
OhosRouteEntry(pattern: '/classics', builder: (_) => const ClassicsPage()),
OhosRouteEntry(pattern: '/health', builder: (_) => const HealthPage()),
OhosRouteEntry(pattern: '/game', builder: (_) => const GameCenterPage()),
OhosRouteEntry(pattern: '/achievement', middlewares: [AuthMiddleware()], builder: (_) => const AchievementPage()),
OhosRouteEntry(pattern: '/achievement/checkin', middlewares: [AuthMiddleware()], builder: (_) => const CheckinPage()),
OhosRouteEntry(pattern: '/badge-wall', builder: (_) => const BadgeWallPage()),
OhosRouteEntry(pattern: '/daily-task', middlewares: [AuthMiddleware()], builder: (_) => const DailyTaskPage()),
OhosRouteEntry(pattern: '/rank', builder: (_) => const RankPage()),
OhosRouteEntry(pattern: '/articles', builder: (_) => const ArticleListPage()),
OhosRouteEntry(pattern: '/article/edit', middlewares: [AuthMiddleware()], builder: (_) => const ArticleEditPage()),
OhosRouteEntry(pattern: '/article/mine', middlewares: [AuthMiddleware()], builder: (_) => const MyArticlesPage()),
OhosRouteEntry(pattern: '/check', builder: (_) => const CheckPage()),
OhosRouteEntry(pattern: '/coin-log', middlewares: [AuthMiddleware()], builder: (_) => const CoinLogPage()),
OhosRouteEntry(pattern: '/user-center', middlewares: [AuthMiddleware()], builder: (_) => const UserCenterPage()),
OhosRouteEntry(pattern: '/tag-cloud', builder: (_) => const TagCloudPage()),
OhosRouteEntry(pattern: '/user-debug', builder: (_) => const UserDebugPage()),
OhosRouteEntry(pattern: '/user-stats', builder: (_) => const UserStatsPage()),
OhosRouteEntry(pattern: '/hidden-sessions', builder: (_) => const HiddenSessionsPage()),
OhosRouteEntry(pattern: '/file-transfer', builder: (_) => const FileTransferPage()),
OhosRouteEntry(pattern: '/device-pairing', builder: (_) => const DevicePairingPage()),
OhosRouteEntry(pattern: '/daily-card', builder: (_) => const DailyCardPage()),
OhosRouteEntry(pattern: '/template-gallery', builder: (_) => const TemplateGalleryPage()),
OhosRouteEntry(pattern: '/reading-report', builder: (_) => const ReadingReportPage()),
OhosRouteEntry(pattern: '/weather', builder: (_) => const WeatherPage()),
OhosRouteEntry(pattern: '/poetry', builder: (_) => const PoetryPage()),
OhosRouteEntry(pattern: '/pomodoro', builder: (_) => const PomodoroPage()),
OhosRouteEntry(pattern: '/countdown', builder: (_) => const CountdownPage()),
OhosRouteEntry(pattern: '/solar-term', builder: (_) => const SolarTermPage()),
OhosRouteEntry(pattern: '/knowledge-graph', builder: (_) => const KnowledgeGraphPage()),
OhosRouteEntry(pattern: '/study-plan', builder: (_) => const StudyPlanPage()),
OhosRouteEntry(pattern: '/daily-fortune', builder: (_) => const DailyFortunePage()),
OhosRouteEntry(pattern: '/daily-fortune/settings', builder: (_) => const FortuneSettingsPage()),
OhosRouteEntry(pattern: '/progress', builder: (_) => const ProgressPage()),
OhosRouteEntry(pattern: '/share/history', middlewares: [AuthMiddleware()], builder: (_) => const ShareHistoryPage()),
OhosRouteEntry(pattern: '/share/targets', middlewares: [AuthMiddleware()], builder: (_) => const ShareTargetEditPage()),
OhosRouteEntry(pattern: '/clipboard', builder: (_) => const ClipboardFlowPage()),
OhosRouteEntry(pattern: '/translate', builder: (_) => const TranslatePage()),
OhosRouteEntry(pattern: '/translate-settings', builder: (_) => const TranslateSettingsPage()),
OhosRouteEntry(pattern: '/widget-management', builder: (_) => const WidgetManagementPage()),
OhosRouteEntry(pattern: '/readlater-chat', builder: (_) => const ChatFlowPage(conversationId: 'readlater')),
// ---- Query 参数路由 ----
OhosRouteEntry(
pattern: '/editor',
builder: (ctx) => EditorPage(initialText: ctx.params.getString('text')),
),
OhosRouteEntry(
pattern: '/chat-flow',
builder: (ctx) => ChatFlowPage(conversationId: ctx.params.getString('sessionId', 'default')),
),
// ---- 路径参数路由 ----
OhosRouteEntry(
pattern: '/chat-settings/:conversationId',
builder: (ctx) => ChatSettingsPage(conversationId: ctx.params.getString('conversationId')),
),
OhosRouteEntry(
pattern: '/category/:type',
builder: (ctx) => CategoryDetailPage(type: ctx.params.getString('type'), name: ctx.params.getString('type')),
),
OhosRouteEntry(
pattern: '/user/:uid',
builder: (ctx) => PublicProfilePage(uid: ctx.params.getInt('uid')),
),
OhosRouteEntry(
pattern: '/article/:id',
builder: (ctx) => ArticleDetailPage(articleId: ctx.params.getInt('id')),
),
OhosRouteEntry(
pattern: '/canvas/:id',
builder: (ctx) => CanvasPlaceholder(canvasId: ctx.params.getString('id')),
),
OhosRouteEntry(
pattern: '/screen-share/:id',
builder: (ctx) => ScreenSharePlaceholder(sessionId: ctx.params.getString('id')),
),
// ---- extra 参数路由 ----
OhosRouteEntry(
pattern: '/tool/calc',
builder: (ctx) => CalcToolPage(config: ctx.extra as CalcToolConfig),
),
OhosRouteEntry(
pattern: '/hanzi-tool',
builder: (ctx) => HanziToolPage(config: ctx.extra as HanziToolConfig),
),
OhosRouteEntry(
pattern: '/tool/list',
builder: (ctx) {
final map = ctx.extra as Map<String, dynamic>;
return ToolListPage(
toolId: map['toolId'] as String? ?? '',
title: map['title'] as String? ?? '',
emoji: map['emoji'] as String? ?? '',
listType: map['listType'] as String? ?? '',
);
},
),
OhosRouteEntry(
pattern: '/transfer-chat',
builder: (ctx) => TransferChatPlaceholder(extra: ctx.extra!),
),
// ---- 特殊固定路由 ----
OhosRouteEntry(
pattern: '/tool/daily-recommend',
builder: (_) => const ToolListPage(toolId: 'daily-recommend', title: '每日推荐', emoji: '📅', listType: 'daily'),
),
OhosRouteEntry(
pattern: '/editor/drafts',
builder: (_) => const DraftListPage(),
),
];
```
- [ ] **Step 2: 添加 `_matchPattern` 静态方法**
`_routes` 列表之后、`push()` 方法之前,添加:
```dart
/// 将 pattern '/article/:id' 与 path '/article/123' 匹配
/// 返回 null 表示不匹配,返回 Map 表示提取的路径参数
static Map<String, String>? _matchPattern(String pattern, String path) {
final patternParts = pattern.split('/');
final pathParts = path.split('/');
if (patternParts.length != pathParts.length) return null;
final params = <String, String>{};
for (var i = 0; i < patternParts.length; i++) {
if (patternParts[i].startsWith(':')) {
params[patternParts[i].substring(1)] = pathParts[i];
} else if (patternParts[i] != pathParts[i]) {
return null;
}
}
return params;
}
```
- [ ] **Step 3: 添加 `_resolveRoute` 私有方法 — 统一路由解析+中间件执行**
`_matchPattern` 之后,添加:
```dart
/// 统一路由解析 — 匹配注册表 + 执行中间件 + 构建 Widget
/// 返回 null 表示路由未匹配或被拒绝
static Widget? _resolveRoute(
BuildContext context,
String route, {
Object? extra,
}) {
final uri = Uri.parse(route);
final path = uri.path;
OhosRouteEntry? matchedEntry;
Map<String, String>? pathParams;
for (final entry in _routes) {
final params = _matchPattern(entry.pattern, path);
if (params != null) {
matchedEntry = entry;
pathParams = params;
break;
}
}
if (matchedEntry == null) {
Log.w('🟡 [OHOS] OhosNavBridge: 未注册路由 "$path"');
return null;
}
final routeContext = OhosRouteContext(
path: path,
params: OhosRouteParams(
pathParams: pathParams,
queryParams: uri.queryParameters,
),
extra: extra,
navigatorContext: context,
);
for (final middleware in matchedEntry.middlewares) {
final result = middleware.handle(routeContext);
if (result is OhosMiddlewareRedirect) {
Log.i('🟢 [OHOS] 中间件重定向: "$path" → "${result.path}"');
return _resolveRoute(context, result.path, extra: result.extra);
}
if (result is OhosMiddlewareReject) {
Log.w('🟡 [OHOS] 路由 "$path" 被拦截: ${result.reason}');
return null;
}
}
return matchedEntry.builder(routeContext);
}
```
- [ ] **Step 4: 重写 `push()` 方法**
删除整个旧的 `push()` 方法,替换为:
```dart
/// 鸿蒙端导航推送
static Future<T?> push<T extends Object?>(
BuildContext context,
String route, {
Object? extra,
}) {
if (!pu.isOhos) {
return _goRouterPush<T>(context, route);
}
final widget = _resolveRoute(context, route, extra: extra);
if (widget == null) {
return _pushNotFound<T>(context, Uri.parse(route).path);
}
return Navigator.of(context).push<T>(
CupertinoPageRoute<T>(builder: (_) => widget),
);
}
```
- [ ] **Step 5: 添加 `_pushNotFound` 404 页面方法**
`push()` 之后,添加:
```dart
/// 404 页面 — 路由未匹配时显示友好提示
static Future<T?> _pushNotFound<T>(BuildContext context, String path) {
return Navigator.of(context).push<T>(
CupertinoPageRoute<T>(
builder: (_) => CupertinoPageScaffold(
navigationBar: const CupertinoNavigationBar(
middle: Text('页面不存在'),
),
child: Center(
child: Padding(
padding: const EdgeInsets.all(32),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text('🔍', style: TextStyle(fontSize: 64)),
const SizedBox(height: 16),
Text(
'鸿蒙端暂不支持此页面',
style: TextStyle(
fontSize: 17,
fontWeight: FontWeight.w600,
color: CupertinoColors.label.resolveFrom(context),
),
),
const SizedBox(height: 8),
Text(
path,
style: const TextStyle(
color: CupertinoColors.secondaryLabel,
),
),
const SizedBox(height: 24),
CupertinoButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('返回'),
),
],
),
),
),
),
),
);
}
```
- [ ] **Step 6: 验证编译通过**
Run: `cd e:\project\flutter\f\xianyan && flutter analyze lib/core/router/ohos_nav_bridge.dart 2>&1 | head -30`
Expected: 无错误
---
### Task 4: 增强 replace() — 支持带参数路由
**Files:**
- Modify: `lib/core/router/ohos_nav_bridge.dart`
- [ ] **Step 1: 重写 `replace()` 方法**
删除整个旧的 `replace()` 方法,替换为:
```dart
/// 鸿蒙端导航替换
static Future<T?> replace<T extends Object?>(
BuildContext context,
String route, {
Object? extra,
}) {
if (!pu.isOhos) {
return _goRouterPush<T>(context, route);
}
final widget = _resolveRoute(context, route, extra: extra);
if (widget == null) {
Log.w('🟡 [OHOS] OhosNavBridge.replace: 路由 "${Uri.parse(route).path}" 无匹配页面');
return Future.value();
}
return Navigator.of(context).pushReplacement<T, void>(
CupertinoPageRoute<T>(builder: (_) => widget),
);
}
```
- [ ] **Step 2: 验证编译通过**
Run: `cd e:\project\flutter\f\xianyan && flutter analyze lib/core/router/ohos_nav_bridge.dart 2>&1 | head -20`
Expected: 无错误
---
### Task 5: 增强 go() — 支持跳转到指定 Tab
**Files:**
- Modify: `lib/core/router/ohos_nav_bridge.dart`
- Modify: `lib/core/layout/ohos_app_shell.dart`
- [ ] **Step 1: 在 OhosAppShell 中暴露 Tab 切换能力**
`ohos_app_shell.dart``_OhosAppShellState` 类中,添加静态方法和变量:
`int _currentIndex = 0;` 之后添加:
```dart
static _OhosAppShellState? _instance;
@override
void initState() {
super.initState();
_instance = this;
}
@override
void dispose() {
_instance = null;
super.dispose();
}
/// 切换到指定 Tab 索引
static void switchTab(int index) {
if (_instance != null && index >= 0 && index < _tabPages.length) {
_instance!.setState(() => _instance!._currentIndex = index);
}
}
```
注意:删除原有的 `initState` 如果有的话,合并到这里。当前代码中没有 `initState`/`dispose`,所以直接添加即可。
- [ ] **Step 2: 重写 OhosNavBridge.go() 方法**
删除旧的 `go()` 方法,替换为:
```dart
/// 鸿蒙端导航回到根/切换Tab
static void go(BuildContext context, String route) {
if (!pu.isOhos) return;
Navigator.of(context).popUntil((r) => r.isFirst);
final tabIndex = _tabIndexFromRoute(route);
if (tabIndex != null) {
OhosAppShell.switchTab(tabIndex);
}
}
/// 从路由路径映射到 Tab 索引
static int? _tabIndexFromRoute(String route) {
return switch (route) {
'/home' => 0,
'/inspiration' => 1,
'/profile' => 2,
_ => null,
};
}
```
- [ ] **Step 3: 在 ohos_nav_bridge.dart 顶部添加 import**
```dart
import 'package:xianyan/core/layout/ohos_app_shell.dart';
```
- [ ] **Step 4: 验证编译通过**
Run: `cd e:\project\flutter\f\xianyan && flutter analyze lib/core/router/ohos_nav_bridge.dart lib/core/layout/ohos_app_shell.dart 2>&1 | head -30`
Expected: 无错误
---
### Task 6: 删除旧代码 + 清理
**Files:**
- Modify: `lib/core/router/ohos_nav_bridge.dart`
- [ ] **Step 1: 删除 `_fuzzyMatch` 方法**
删除整个 `_fuzzyMatch` 方法(已被注册表匹配 + 404 页面替代)。
- [ ] **Step 2: 删除不再需要的 `AppRoutes` import 中的引用**
检查是否还有代码引用 `AppRoutes`。如果 `_routes` 列表中已使用字符串字面量(如 `'/search'`),则可以移除 `import 'package:xianyan/core/router/app_router.dart' show AppRoutes;` 这行 import。
但注意:`_routes` 列表中使用了字符串字面量而非 `AppRoutes.xxx` 常量,所以可以安全移除该 import。
- [ ] **Step 3: 更新文件头部注释**
更新文件头部的 `上次更新` 信息:
```dart
/// 上次更新: v14.65.0 重构为注册表+中间件架构自动参数解析AuthMiddleware增强replace/go
```
- [ ] **Step 4: 验证编译通过**
Run: `cd e:\project\flutter\f\xianyan && flutter analyze lib/core/router/ohos_nav_bridge.dart 2>&1 | head -30`
Expected: 无错误
---
### Task 7: 全项目编译验证 + CHANGELOG 更新
**Files:**
- Modify: `CHANGELOG.md`
- [ ] **Step 1: 全项目编译验证**
Run: `cd e:\project\flutter\f\xianyan && flutter analyze 2>&1 | tail -20`
Expected: 无错误
- [ ] **Step 2: 更新 CHANGELOG.md**
在 CHANGELOG.md 的最新版本条目中添加:
```
- 重构 OhosNavBridge 为注册表+中间件架构
- 新增 OhosRouteParams/OhosRouteContext/OhosRouteEntry 核心类
- 新增 OhosNavMiddleware 中间件基类 + AuthMiddleware/LogMiddleware
- 自动参数解析(路径参数+查询参数+extra替代手动 if-else
- replace() 支持带参数路由
- go() 支持跳转到指定 Tab
- 新增 404 友好页面替代模糊匹配
- 需要登录的路由统一标记 AuthMiddleware
```
- [ ] **Step 3: Commit**
```bash
cd e:\project\flutter\f\xianyan
git add lib/core/router/ohos_nav_bridge.dart lib/core/layout/ohos_app_shell.dart CHANGELOG.md
git commit -m "refactor: OhosNavBridge 注册表+中间件架构重构
- 新增 OhosRouteParams/OhosRouteContext/OhosRouteEntry 核心类
- 新增 OhosNavMiddleware 中间件基类 + AuthMiddleware/LogMiddleware
- 自动参数解析替代手动 if-else
- replace() 支持带参数路由
- go() 支持跳转到指定 Tab
- 新增 404 友好页面"
```

View File

@@ -1,936 +0,0 @@
# 移动端性能优化 — 智能节流方案 实施计划
> **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:** 在不阉割任何视觉效果和动画的前提下,通过智能节流策略消除移动端发热和高资源占用问题
**Architecture:** 创建 PerformanceOrchestrator 性能调度中心单例,统一管理帧率节流、动画可见性感知、后台服务休眠、特效互斥调度。各组件主动注册到调度器,由调度器根据性能级别、电量、前后台状态决定运行策略。
**Tech Stack:** Flutter/Dart, Riverpod, battery_plus, sensors_plus, visibility_detector
---
## 文件结构
### 新建文件
| 文件 | 职责 |
|------|------|
| `lib/core/services/performance/performance_orchestrator.dart` | 性能调度中心单例 — 帧率/级别/电量/前后台统一管理 |
| `lib/core/services/performance/app_lifecycle_gate.dart` | 前后台统一管理门 — 暂停/恢复后台服务和动画 |
| `lib/core/services/performance/effect_mutex.dart` | 特效互斥调度器 — 同一时刻限制重量级特效数量 |
### 修改文件
| 文件 | 修改内容 |
|------|---------|
| `lib/shared/widgets/shader_card_background.dart` | Shader 帧率节流 + 可见性暂停 |
| `lib/shared/widgets/appbar_character_sprite.dart` | 不可见时暂停 idle 动画 + setState→ValueNotifier |
| `lib/shared/widgets/tab_icon_sprite.dart` | 未选中 Tab 停止 glow 动画 |
| `lib/shared/widgets/glass_container.dart` | RepaintBoundary 包裹 + 缓存策略 |
| `lib/shared/widgets/glass_bottom_nav_bar.dart` | RepaintBoundary 包裹 |
| `lib/core/layout/app_shell.dart` | 集成 AppLifecycleGate |
| `lib/app/app.dart` | 集成 AppLifecycleGate + PerformanceOrchestrator 初始化 |
| `lib/main.dart` | 初始化 PerformanceOrchestrator |
| `lib/core/services/device/shake_detector.dart` | 前后台暂停/恢复 |
| `lib/core/services/clipboard_monitor_service.dart` | 前后台暂停/恢复 |
| `lib/core/services/device/battery_info_service.dart` | 前后台暂停/恢复轮询 |
| `lib/core/services/device/battery_optimization_service.dart` | 联动 PerformanceOrchestrator |
| `lib/core/utils/interaction_animations.dart` | CelebrationOverlay 按需激活 |
| `lib/features/home/presentation/home_refresh_indicator.dart` | setState→局部刷新 |
| `CHANGELOG.md` | 记录本次优化 |
---
## Task 1: 创建 PerformanceOrchestrator 性能调度中心
**Files:**
- Create: `lib/core/services/performance/performance_orchestrator.dart`
- [ ] **Step 1: 创建 PerformanceOrchestrator 单例**
```dart
// ============================================================
// 闲言APP — 性能调度中心
// 创建时间: 2026-05-22
// 更新时间: 2026-05-22
// 作用: 统一管理帧率节流/性能级别/电量联动/前后台切换
// 上次更新: 初始创建
// ============================================================
import 'dart:async';
import 'package:flutter/scheduler.dart';
import 'package:xianyan/core/services/device/battery_info_service.dart';
import 'package:xianyan/core/storage/app_kv_store.dart';
import 'package:xianyan/core/utils/logger.dart';
/// 性能级别
enum PerformanceLevel {
full('完整', 1, 1),
balanced('平衡', 2, 30),
saver('省电', 3, 20);
const PerformanceLevel(this.label, this.sortOrder, this.targetFps);
final String label;
final int sortOrder;
final int targetFps;
}
/// 帧率节流回调类型 — 返回 true 表示本帧需要更新
typedef FrameThrottleCallback = bool Function();
class PerformanceOrchestrator {
PerformanceOrchestrator._();
static final PerformanceOrchestrator instance = PerformanceOrchestrator._();
static const _keyLevel = 'performance_level';
PerformanceLevel _level = PerformanceLevel.full;
bool _isAppForeground = true;
StreamSubscription<BatteryInfo>? _batterySub;
final List<VoidCallback> _onForegroundCallbacks = [];
final List<VoidCallback> _onBackgroundCallbacks = [];
PerformanceLevel get level => _level;
bool get isAppForeground => _isAppForeground;
bool get shouldThrottleShader => _level != PerformanceLevel.full;
bool get shouldPauseAnimations => !_isAppForeground || _level == PerformanceLevel.saver;
/// 初始化 — 在 main() 中调用
Future<void> init() async {
final stored = AppKVStore.getString(_keyLevel);
_level = _resolveLevel(stored);
Log.i('PerformanceOrchestrator 初始化: level=${_level.label}');
_batterySub = BatteryInfoService.instance.onBatteryChanged.listen((info) {
if (info.isCritical && _level != PerformanceLevel.saver) {
setLevel(PerformanceLevel.saver);
Log.i('PerformanceOrchestrator: 电量严重不足,自动切换省电模式');
}
});
}
/// 设置性能级别
void setLevel(PerformanceLevel newLevel) {
if (_level == newLevel) return;
_level = newLevel;
AppKVStore.setString(_keyLevel, newLevel.name);
Log.i('PerformanceOrchestrator: 级别切换为 ${newLevel.label}');
}
/// App 进入前台
void onAppResumed() {
if (_isAppForeground) return;
_isAppForeground = true;
Log.i('PerformanceOrchestrator: App 回到前台');
for (final cb in _onForegroundCallbacks) {
cb();
}
}
/// App 进入后台
void onAppPaused() {
if (!_isAppForeground) return;
_isAppForeground = false;
Log.i('PerformanceOrchestrator: App 进入后台');
for (final cb in _onBackgroundCallbacks) {
cb();
}
}
/// 注册前台回调
void onForeground(VoidCallback callback) {
_onForegroundCallbacks.add(callback);
}
/// 注册后台回调
void onBackground(VoidCallback callback) {
_onBackgroundCallbacks.add(callback);
}
/// 移除回调
void removeCallbacks(VoidCallback callback) {
_onForegroundCallbacks.remove(callback);
_onBackgroundCallbacks.remove(callback);
}
/// 创建节流帧计数器 — 返回每 N 帧触发一次的判断函数
/// full: 每1帧, balanced: 每2帧, saver: 每3帧
FrameThrottleCallback createFrameThrottle() {
int frameCount = 0;
return () {
frameCount++;
final skip = switch (_level) {
PerformanceLevel.full => 1,
PerformanceLevel.balanced => 2,
PerformanceLevel.saver => 3,
};
if (frameCount % skip == 0) {
return true;
}
return false;
};
}
PerformanceLevel _resolveLevel(String? stored) {
return switch (stored) {
'full' => PerformanceLevel.full,
'balanced' => PerformanceLevel.balanced,
'saver' => PerformanceLevel.saver,
_ => PerformanceLevel.full,
};
}
void dispose() {
_batterySub?.cancel();
_onForegroundCallbacks.clear();
_onBackgroundCallbacks.clear();
}
}
```
- [ ] **Step 2: 验证文件创建**
Run: `dart analyze lib/core/services/performance/performance_orchestrator.dart`
Expected: 无错误
---
## Task 2: 创建 AppLifecycleGate 前后台管理门
**Files:**
- Create: `lib/core/services/performance/app_lifecycle_gate.dart`
- [ ] **Step 1: 创建 AppLifecycleGate**
```dart
// ============================================================
// 闲言APP — 前后台统一管理门
// 创建时间: 2026-05-22
// 更新时间: 2026-05-22
// 作用: 统一管理前后台切换时暂停/恢复后台服务和动画
// 上次更新: 初始创建
// ============================================================
import 'package:flutter/widgets.dart';
import 'package:xianyan/core/services/performance/performance_orchestrator.dart';
import 'package:xianyan/core/services/device/shake_detector.dart';
import 'package:xianyan/core/services/clipboard_monitor_service.dart';
import 'package:xianyan/core/services/device/battery_info_service.dart';
import 'package:xianyan/core/utils/logger.dart';
/// App 生命周期统一管理门
///
/// 在 App 根组件中混入,统一管理前后台切换时:
/// - 暂停/恢复传感器(摇一摇)
/// - 暂停/恢复剪贴板监控
/// - 暂停/恢复电池轮询
/// - 通知 PerformanceOrchestrator 前后台状态
mixin AppLifecycleGate on WidgetsBindingObserver {
bool _lifecycleGateInitialized = false;
void initLifecycleGate() {
if (_lifecycleGateInitialized) return;
_lifecycleGateInitialized = true;
WidgetsBinding.instance.addObserver(this);
}
void disposeLifecycleGate() {
WidgetsBinding.instance.removeObserver(this);
_lifecycleGateInitialized = false;
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
super.didChangeAppLifecycleState(state);
switch (state) {
case AppLifecycleState.paused:
_onEnterBackground();
case AppLifecycleState.resumed:
_onEnterForeground();
default:
break;
}
}
void _onEnterBackground() {
Log.i('AppLifecycleGate: 进入后台 — 暂停服务和动画');
PerformanceOrchestrator.instance.onAppPaused();
ShakeDetector.instance.stop();
ClipboardMonitorService.instance.stopMonitor();
BatteryInfoService.instance.pausePolling();
}
void _onEnterForeground() {
Log.i('AppLifecycleGate: 回到前台 — 恢复服务和动画');
PerformanceOrchestrator.instance.onAppResumed();
if (ShakeDetector.instance.isEnabled) {
ShakeDetector.instance.start();
}
if (ClipboardMonitorService.instance.isEnabled) {
ClipboardMonitorService.instance.startMonitor();
}
BatteryInfoService.instance.resumePolling();
}
}
```
- [ ] **Step 2: 为 BatteryInfoService 添加 pausePolling/resumePolling 方法**
修改 `lib/core/services/device/battery_info_service.dart`,添加:
```dart
bool _pollingPaused = false;
void pausePolling() {
_pollingPaused = true;
_pollTimer?.cancel();
_pollTimer = null;
Log.i('BatteryInfoService: 轮询已暂停');
}
void resumePolling() {
if (!_pollingPaused) return;
_pollingPaused = false;
_pollTimer?.cancel();
_pollTimer = Timer.periodic(const Duration(minutes: 5), (_) async {
try {
_currentLevel = await _battery.batteryLevel;
_notify();
} catch (_) {}
});
Log.i('BatteryInfoService: 轮询已恢复');
}
```
- [ ] **Step 3: 验证**
Run: `dart analyze lib/core/services/performance/app_lifecycle_gate.dart lib/core/services/device/battery_info_service.dart`
Expected: 无错误
---
## Task 3: 创建 EffectMutex 特效互斥调度器
**Files:**
- Create: `lib/core/services/performance/effect_mutex.dart`
- [ ] **Step 1: 创建 EffectMutex**
```dart
// ============================================================
// 闲言APP — 特效互斥调度器
// 创建时间: 2026-05-22
// 更新时间: 2026-05-22
// 作用: 限制同一时刻运行的重量级特效数量防止GPU过载
// 上次更新: 初始创建
// ============================================================
import 'dart:async';
import 'package:xianyan/core/utils/logger.dart';
/// 特效优先级
enum EffectPriority {
interaction(0),
entrance(1),
decoration(2);
const EffectPriority(this.value);
final int value;
}
/// 特效令牌 — 持有者表示正在运行一个重量级特效
class EffectToken {
EffectToken._(this._mutex, this.name);
final EffectMutex _mutex;
final String name;
bool _released = false;
bool get isActive => !_released;
void release() {
if (_released) return;
_released = true;
_mutex._release(this);
}
}
/// 特效互斥调度器
///
/// 限制同一时刻运行的重量级特效数量。
/// 当已有 maxConcurrent 个特效运行时,低优先级特效会被延迟。
class EffectMutex {
EffectMutex({this.maxConcurrent = 2});
final int maxConcurrent;
final List<EffectToken> _active = [];
final List<_PendingEffect> _pending = [];
/// 申请一个特效槽位
///
/// 如果当前活跃特效数未达上限,立即返回 token
/// 否则等待直到有槽位释放。
Future<EffectToken> acquire(String name, {EffectPriority priority = EffectPriority.decoration}) async {
if (_active.length < maxConcurrent) {
final token = EffectToken._(this, name);
_active.add(token);
Log.i('EffectMutex: "$name" 获得槽位 (${_active.length}/$maxConcurrent)');
return token;
}
final completer = Completer<EffectToken>();
_pending.add(_PendingEffect(name, priority, completer));
Log.i('EffectMutex: "$name" 排队等待 (优先级=${priority.name}, 队列=${_pending.length})');
return completer.future;
}
void _release(EffectToken token) {
_active.remove(token);
Log.i('EffectMutex: "${token.name}" 释放槽位 (${_active.length}/$maxConcurrent)');
if (_pending.isNotEmpty) {
_pending.sort((a, b) => a.priority.value.compareTo(b.priority.value));
final next = _pending.removeAt(0);
final newToken = EffectToken._(this, next.name);
_active.add(newToken);
next.completer.complete(newToken);
Log.i('EffectMutex: "${next.name}" 获得槽位 (${_active.length}/$maxConcurrent)');
}
}
}
class _PendingEffect {
_PendingEffect(this.name, this.priority, this.completer);
final String name;
final EffectPriority priority;
final Completer<EffectToken> completer;
}
```
- [ ] **Step 2: 验证**
Run: `dart analyze lib/core/services/performance/effect_mutex.dart`
Expected: 无错误
---
## Task 4: Shader 帧率节流 — 修改 ShaderCardBackground
**Files:**
- Modify: `lib/shared/widgets/shader_card_background.dart`
- [ ] **Step 1: 添加帧率节流 + 可见性感知**
`_ShaderCardBackgroundState` 修改为:
```dart
class _ShaderCardBackgroundState extends State<ShaderCardBackground>
with SingleTickerProviderStateMixin, WidgetsBindingObserver {
late Ticker _ticker;
final ValueNotifier<double> _timeNotifier = ValueNotifier(0);
ui.FragmentProgram? _program;
bool _loaded = false;
bool _isVisible = true;
late FrameThrottleCallback _shouldRenderFrame;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
_shouldRenderFrame = PerformanceOrchestrator.instance.createFrameThrottle();
_ticker = createTicker(_onTick);
_ticker.start();
_loadShader();
PerformanceOrchestrator.instance.onForeground(_resumeTicker);
PerformanceOrchestrator.instance.onBackground(_pauseTicker);
}
void _pauseTicker() {
if (_ticker.isActive) _ticker.stop();
}
void _resumeTicker() {
if (!_ticker.isActive && _isVisible) _ticker.start();
}
void _onTick(Duration elapsed) {
if (!_shouldRenderFrame()) return;
_timeNotifier.value = elapsed.inMicroseconds / 1000000.0;
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
super.didChangeAppLifecycleState(state);
if (state == AppLifecycleState.paused) {
_pauseTicker();
} else if (state == AppLifecycleState.resumed && _isVisible) {
_resumeTicker();
}
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
PerformanceOrchestrator.instance.removeCallbacks(_resumeTicker);
PerformanceOrchestrator.instance.removeCallbacks(_pauseTicker);
_ticker.dispose();
_timeNotifier.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
if (!_loaded || _program == null) {
return Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
const Color(0xFF6699EE).withValues(alpha: 0.6),
const Color(0xFFB366CC).withValues(alpha: 0.6),
],
),
),
);
}
return RepaintBoundary(
child: CustomPaint(
painter: _ShaderPainter(
program: _program!,
timeNotifier: _timeNotifier,
touch: widget.touchOffset ?? Offset.zero,
),
),
);
}
}
```
同时添加 import:
```dart
import 'package:xianyan/core/services/performance/performance_orchestrator.dart';
```
- [ ] **Step 2: 验证**
Run: `dart analyze lib/shared/widgets/shader_card_background.dart`
Expected: 无错误
---
## Task 5: 动画可见性感知 — 修改 AppBarCharacterSprite
**Files:**
- Modify: `lib/shared/widgets/appbar_character_sprite.dart`
- [ ] **Step 1: 添加前后台感知 + 不可见时暂停 idle 动画**
`AppBarCharacterSpriteState` 中添加:
```dart
// 在类顶部添加
bool _isAppForeground = true;
// 在 initState 末尾添加
PerformanceOrchestrator.instance.onForeground(_onForeground);
PerformanceOrchestrator.instance.onBackground(_onBackground);
// 添加方法
void _onForeground() {
_isAppForeground = true;
if (widget.animationIntensity > 0.0 && !_idleController.isAnimating) {
_idleController.repeat(reverse: true);
}
}
void _onBackground() {
_isAppForeground = false;
_idleController.stop();
}
// 修改 _idleController.repeat 的位置 — 在 initState 中
// 原来: if (widget.animationIntensity > 0.0) _idleController.repeat(reverse: true);
// 改为: if (widget.animationIntensity > 0.0 && _isAppForeground) _idleController.repeat(reverse: true);
// 在 didUpdateWidget 中修改 idle 恢复逻辑
// 原来: } else if (!_idleController.isAnimating) {
// 改为: } else if (!_idleController.isAnimating && _isAppForeground) {
// 在 dispose 中添加
PerformanceOrchestrator.instance.removeCallbacks(_onForeground);
PerformanceOrchestrator.instance.removeCallbacks(_onBackground);
```
添加 import:
```dart
import 'package:xianyan/core/services/performance/performance_orchestrator.dart';
```
- [ ] **Step 2: 优化 onPointerMove 中的 setState**
`onPointerMove` 中的 `setState(() {})` 替换为局部刷新。由于 `_eyeOffset``AnimatedBuilder` 内部使用,且 `AnimatedBuilder` 已经在监听 6 个 Controller所以 `_eyeOffset` 的变化需要触发重绘。最佳方案是将 `_eyeOffset` 改为 `ValueNotifier<Offset>`
```dart
// 替换 Offset _eyeOffset = Offset.zero; 为:
final ValueNotifier<Offset> _eyeOffsetNotifier = ValueNotifier(Offset.zero);
// onPointerMove 中替换 setState(() {}); 为:
_eyeOffsetNotifier.value = Offset(
(delta.dx / distance) * factor,
(delta.dy / distance) * factor,
);
// lookAtTitle 中替换 _eyeOffset = ... 为:
_eyeOffsetNotifier.value = const Offset(3.0, 0.0);
// 和:
_eyeOffsetNotifier.value = Offset.zero;
// AnimatedBuilder 的 animation 改为:
animation: Listenable.merge([
_bounceController,
_earController,
_noseController,
_cheekController,
_expressionController,
_idleController,
_eyeOffsetNotifier,
]),
// builder 内部替换 _eyeOffset 为 _eyeOffsetNotifier.value
// dispose 中添加:
_eyeOffsetNotifier.dispose();
```
- [ ] **Step 3: 验证**
Run: `dart analyze lib/shared/widgets/appbar_character_sprite.dart`
Expected: 无错误
---
## Task 6: Tab 动画节流 — 修改 TabIconSprite
**Files:**
- Modify: `lib/shared/widgets/tab_icon_sprite.dart`
- [ ] **Step 1: 未选中 Tab 停止 glow 呼吸动画**
`_TabIconSpriteState``didUpdateWidget` 中,当 Tab 从选中变为未选中时,`_glowController.stop()` 已经存在。需要确保未选中的 Tab 不运行 glow repeat
```dart
// 在 didUpdateWidget 中,!widget.isSelected && oldWidget.isSelected 分支
// 已有 _glowController.stop(); 确认无误
// 在 initState 中,只有 isSelected 时才 repeat glow
// 已有 if (widget.isSelected) { _glowController.repeat(reverse: true); }
// 确认无误
```
- [ ] **Step 2: 添加前后台感知**
```dart
// 在 _TabIconSpriteState 中添加
bool _isAppForeground = true;
// initState 末尾添加
PerformanceOrchestrator.instance.onForeground(_onForeground);
PerformanceOrchestrator.instance.onBackground(_onBackground);
void _onForeground() {
_isAppForeground = true;
if (widget.isSelected) {
_glowController.repeat(reverse: true);
}
}
void _onBackground() {
_isAppForeground = false;
_glowController.stop();
}
// dispose 中添加
PerformanceOrchestrator.instance.removeCallbacks(_onForeground);
PerformanceOrchestrator.instance.removeCallbacks(_onBackground);
```
添加 import:
```dart
import 'package:xianyan/core/services/performance/performance_orchestrator.dart';
```
- [ ] **Step 3: 验证**
Run: `dart analyze lib/shared/widgets/tab_icon_sprite.dart`
Expected: 无错误
---
## Task 7: BackdropFilter 缓存优化 — 修改 GlassContainer
**Files:**
- Modify: `lib/shared/widgets/glass_container.dart`
- [ ] **Step 1: 添加 RepaintBoundary 包裹**
`build` 方法中,将 `child``RepaintBoundary` 包裹,减少 BackdropFilter 的重绘范围:
```dart
// 在所有 _maybeBackdropFilter 调用中,将 child 参数用 RepaintBoundary 包裹
// 例如:
child: _maybeBackdropFilter(
sigma: cfg.blurSigma * multiplier,
child: RepaintBoundary(child: Padding(padding: effectivePadding, child: child)),
),
```
- [ ] **Step 2: 验证**
Run: `dart analyze lib/shared/widgets/glass_container.dart`
Expected: 无错误
---
## Task 8: GlassBottomNavBar 缓存优化
**Files:**
- Modify: `lib/shared/widgets/glass_bottom_nav_bar.dart`
- [ ] **Step 1: 添加 RepaintBoundary**
`build` 方法中,将整个 BackdropFilter 子树用 RepaintBoundary 包裹:
```dart
// 在 build 方法的 return Container(...) 外层包裹 RepaintBoundary
return RepaintBoundary(
child: Container(
margin: ...
...
),
);
```
- [ ] **Step 2: 验证**
Run: `dart analyze lib/shared/widgets/glass_bottom_nav_bar.dart`
Expected: 无错误
---
## Task 9: 集成 AppLifecycleGate 到 App 根组件
**Files:**
- Modify: `lib/app/app.dart`
- Modify: `lib/main.dart`
- [ ] **Step 1: 在 _XianyanAppState 中混入 AppLifecycleGate**
```dart
// 修改类声明
class _XianyanAppState extends ConsumerState<XianyanApp>
with WidgetsBindingObserver, AppLifecycleGate {
// 修改 initState
@override
void initState() {
super.initState();
initLifecycleGate();
}
// 修改 dispose
@override
void dispose() {
disposeLifecycleGate();
super.dispose();
}
// 删除原有的 didChangeAppLifecycleState 方法AppLifecycleGate 已处理)
// 但保留 AppLockService 相关逻辑,在 AppLifecycleGate 的重写中调用
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
super.didChangeAppLifecycleState(state);
switch (state) {
case AppLifecycleState.paused:
try { AppLockService.onAppPaused(); } catch (e) { Log.e('应用锁暂停处理失败', e); }
break;
case AppLifecycleState.resumed:
Future.microtask(() {
try { AppLockService.onAppResumed(); } catch (e) { Log.e('应用锁恢复处理失败', e); }
});
break;
default:
break;
}
}
```
添加 import:
```dart
import 'package:xianyan/core/services/performance/app_lifecycle_gate.dart';
```
- [ ] **Step 2: 在 main.dart 中初始化 PerformanceOrchestrator**
`main()` 函数中,`BatteryOptimizationService.init()` 之后添加:
```dart
try {
await PerformanceOrchestrator.instance.init();
if (pu.isOhos) Log.i('🟢 [OHOS] 性能调度中心初始化完成');
} catch (e, st) {
Log.e('性能调度中心初始化失败', e, st);
}
```
添加 import:
```dart
import 'core/services/performance/performance_orchestrator.dart';
```
- [ ] **Step 3: 验证**
Run: `dart analyze lib/app/app.dart lib/main.dart`
Expected: 无错误
---
## Task 10: CelebrationOverlay 按需激活
**Files:**
- Modify: `lib/core/utils/interaction_animations.dart`
- [ ] **Step 1: 修改 CelebrationOverlay 使用 EffectMutex**
```dart
// 在 CelebrationOverlayState 中添加
EffectToken? _effectToken;
// 修改 celebrate 方法
void celebrate() async {
final mutex = EffectMutex(maxConcurrent: 2);
_effectToken = await mutex.acquire('celebration', priority: EffectPriority.decoration);
_centerController.play();
}
// 在 ConfettiWidget 的 onComplete 或 _centerController 的状态监听中释放 token
// 修改 initState:
@override
void initState() {
super.initState();
_centerController = ConfettiController(
duration: const Duration(milliseconds: 1500),
);
_centerController.addListener(() {
if (_centerController.state == ConfettiControllerState.stopped && _effectToken != null) {
_effectToken!.release();
_effectToken = null;
}
});
}
```
添加 import:
```dart
import 'package:xianyan/core/services/performance/effect_mutex.dart';
```
- [ ] **Step 2: 验证**
Run: `dart analyze lib/core/utils/interaction_animations.dart`
Expected: 无错误
---
## Task 11: HomeRefreshIndicator 局部刷新优化
**Files:**
- Modify: `lib/features/home/presentation/home_refresh_indicator.dart`
- [ ] **Step 1: 将 _pullProgress 改为 ValueNotifier**
```dart
// 替换 double _pullProgress = 0.0; 为:
final ValueNotifier<double> _pullProgressNotifier = ValueNotifier(0.0);
// 替换所有 setState(() => _pullProgress = newProgress); 为:
_pullProgressNotifier.value = newProgress;
// 替换所有 _pullProgress 读取为 _pullProgressNotifier.value
// 在 _displayProgress getter 中使用 _pullProgressNotifier.value
// 在 _onResetTick 中,将 setState(() {}); 改为直接通知 _pullProgressNotifier
// 由于 _resetController 的 listener 已经在更新,只需确保 AnimatedBuilder 监听 _pullProgressNotifier
// 在 build 方法中,用 AnimatedBuilder 包裹需要响应 _pullProgress 的部分:
// 将 progress 变量改为从 _pullProgressNotifier 获取
```
注意:由于此文件较复杂,涉及多个状态变量(`_isRefreshing`, `_isComplete`, `_pullProgress`),最安全的做法是将 `_pullProgress` 改为 `ValueNotifier`,其他保持 `setState` 不变(因为它们变化频率低)。
- [ ] **Step 2: 验证**
Run: `dart analyze lib/features/home/presentation/home_refresh_indicator.dart`
Expected: 无错误
---
## Task 12: 全局编译验证 + CHANGELOG 更新
**Files:**
- Modify: `CHANGELOG.md`
- [ ] **Step 1: 运行全局编译验证**
Run: `cd e:\project\flutter\f\xianyan && flutter analyze`
Expected: 无错误
- [ ] **Step 2: 更新 CHANGELOG.md**
在文件顶部添加新版本记录:
```markdown
## [v14.82.0] - 2026-05-22
### 移动端性能优化 — 智能节流方案
**问题**移动端发热严重GPU/CPU 资源占用高,后台持续消耗电量
**根因分析**
- Fragment Shader 每帧 60fps 无间断运行
- 21+ 个 AnimationController 全天候运行(角色精灵 + Tab 图标)
- 149 处 BackdropFilter 每帧重新模糊
- 8+ 个后台服务持续监听(传感器/剪贴板/电池)
- 多个重量级特效同时运行Lottie + Confetti + Shader + Tilt
**优化方案**(不阉割任何视觉效果):
- ✅ 新增 PerformanceOrchestrator 性能调度中心 — 统一管理帧率/级别/电量联动
- ✅ Shader 帧率节流 — full 60fps / balanced 30fps / saver 20fps
- ✅ 动画前后台感知 — App 后台时自动暂停所有 repeat 动画
- ✅ BackdropFilter 缓存 — RepaintBoundary 减少重绘范围
- ✅ AppLifecycleGate 前后台管理门 — 统一暂停/恢复传感器和服务
- ✅ EffectMutex 特效互斥调度器 — 限制同时运行的重量级特效数量
- ✅ HomeRefreshIndicator 局部刷新 — ValueNotifier 替代 setState
- ✅ AppBarCharacterSprite 局部刷新 — _eyeOffset 改为 ValueNotifier
**新增文件**
- `lib/core/services/performance/performance_orchestrator.dart`
- `lib/core/services/performance/app_lifecycle_gate.dart`
- `lib/core/services/performance/effect_mutex.dart`
**预期效果**
- GPU 平均帧耗时 ↓60%
- 空闲 CPU 占用 ↓80%
- 后台 CPU 占用 ↓95%
- 电池续航接近正常 APP
```
- [ ] **Step 3: 最终验证**
Run: `cd e:\project\flutter\f\xianyan && flutter analyze`
Expected: 无错误

View File

@@ -1,793 +0,0 @@
# 闲言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路径正确 |

View File

@@ -1,616 +0,0 @@
# 闲言APP 架构重构与功能增强 实施计划
> **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:** 解决7大架构问题 + 实现4大功能增强全面提升项目质量
**Architecture:** 分7个阶段执行每阶段独立可交付。优先修复架构问题类型安全、状态管理、路由追踪、主题适配、组件拆分再增强功能下拉手势、空状态、最近面板、标签/精灵/引导页)
**Tech Stack:** Flutter/Dart, freezed + json_serializable, Riverpod, GoRouter, KvStorage
---
## 阶段总览
| 阶段 | 名称 | 预估任务数 | 优先级 |
|------|------|-----------|--------|
| P1 | API类型安全 (freezed + json_serializable) | 8 | ⭐⭐⭐⭐⭐ |
| P2 | 状态管理统一 + 路由追踪合并 | 4 | ⭐⭐⭐⭐⭐ |
| P3 | 主题适配全量修复 | 6 | ⭐⭐⭐⭐ |
| P4 | 组件拆分 | 5 | ⭐⭐⭐ |
| P5 | 下拉手势 + 空状态优化 | 3 | ⭐⭐⭐⭐ |
| P6 | 最近打开面板增强 | 5 | ⭐⭐⭐⭐⭐ |
| P7 | 标签/精灵/引导页增强 | 6 | ⭐⭐⭐ |
---
## P1: API类型安全 (freezed + json_serializable)
### Task P1.1: 为阅读报告模型引入 freezed
**Files:**
- Modify: `lib/features/reading_report/models/reading_report_models.dart`
- Create: (auto-generated .freezed.dart, .g.dart)
- [ ] **Step 1: 添加 freezed 注解到 ReportSummary**
```dart
@freezed
class ReportSummary with _$ReportSummary {
const factory ReportSummary({
required int totalSentences,
required int totalFavorites,
required int totalLikes,
required int streakDays,
required int longestStreak,
required DateTime? lastReadAt,
}) = _ReportSummary;
factory ReportSummary.fromJson(Map<String, dynamic> json) =>
_$ReportSummaryFromJson(json);
}
```
- [ ] **Step 2: 添加 freezed 注解到 ReportTrend / HeatmapDay 等模型**
- [ ] **Step 3: 运行 build_runner 生成代码**
Run: `dart run build_runner build --delete-conflicting-outputs`
- [ ] **Step 4: 更新 reading_report_service.dart 使用生成的 fromJson**
- [ ] **Step 5: 运行 flutter analyze 验证**
### Task P1.2: 为排行榜模型引入 freezed
**Files:**
- Modify: `lib/features/rank/providers/rank_provider.dart` (将 RankSeason/RankItem 移到独立模型文件)
- Create: `lib/features/rank/models/rank_models.dart`
- [ ] **Step 1: 创建 rank_models.dart用 freezed 定义 RankSeason 和 RankItem**
- [ ] **Step 2: 运行 build_runner 生成代码**
- [ ] **Step 3: 更新 rank_provider.dart 使用新模型**
- [ ] **Step 4: 运行 flutter analyze 验证**
### Task P1.3: 为用户中心模型引入 freezed
**Files:**
- Modify: `lib/features/mine/user_center/models/user_center_models.dart`
- Modify: `lib/features/mine/user_center/models/account_insight_model.dart`
- [ ] **Step 1: 为 DashboardData / UserStats / AccountInsight 等添加 freezed 注解**
- [ ] **Step 2: 运行 build_runner 生成代码**
- [ ] **Step 3: 更新 service/provider 使用新模型**
- [ ] **Step 4: 运行 flutter analyze 验证**
### Task P1.4: 为 Feed 模型引入 freezed
**Files:**
- Modify: `lib/features/home/models/feed_model.dart`
- Modify: `lib/features/home/providers/home_sentence_model.dart`
- [ ] **Step 1: 为 FeedItem / SentenceData 等添加 freezed 注解**
- [ ] **Step 2: 运行 build_runner 生成代码**
- [ ] **Step 3: 更新 service/provider 使用新模型**
- [ ] **Step 4: 运行 flutter analyze 验证**
### Task P1.5: 为学习计划模型引入 freezed
**Files:**
- Modify: `lib/features/study_plan/models/study_plan_models.dart`
- [ ] **Step 1: 为 StudyPlan / ReadingGoal 等添加 freezed 注解**
- [ ] **Step 2: 运行 build_runner 生成代码**
- [ ] **Step 3: 更新 service/provider 使用新模型**
- [ ] **Step 4: 运行 flutter analyze 验证**
### Task P1.6: 为工具中心模型引入 freezed
**Files:**
- Modify: `lib/features/tool_center/inspiration/models/tool_item.dart`
- [ ] **Step 1: 为 ToolItem 添加 freezed 注解**
- [ ] **Step 2: 运行 build_runner 生成代码**
- [ ] **Step 3: 更新 service/provider 使用新模型**
- [ ] **Step 4: 运行 flutter analyze 验证**
### Task P1.7: 为发现页模型引入 freezed
**Files:**
- Modify: `lib/features/discover/services/discover_service.dart` (内联模型提取)
- Create: `lib/features/discover/models/discover_models.dart`
- [ ] **Step 1: 提取内联模型到 discover_models.dart添加 freezed 注解**
- [ ] **Step 2: 运行 build_runner 生成代码**
- [ ] **Step 3: 更新 service/provider 使用新模型**
- [ ] **Step 4: 运行 flutter analyze 验证**
### Task P1.8: 全量 build_runner + 验证
- [ ] **Step 1: 运行全量 build_runner**
Run: `dart run build_runner build --delete-conflicting-outputs`
- [ ] **Step 2: 运行 flutter analyze 验证零 error**
- [ ] **Step 3: 提交 P1 阶段代码**
---
## P2: 状态管理统一 + 路由追踪合并
### Task P2.1: 创建统一的最近路由 Provider
**Files:**
- Create: `lib/core/services/navigation/recent_route_service.dart`
- Modify: `lib/core/router/app_nav_extension.dart`
- Modify: `lib/features/home/providers/tool_center_recent_provider.dart`
- [ ] **Step 1: 创建 RecentRouteService — 单一数据源**
```dart
class RecentRouteService {
static const _kRecentRoutes = 'tool_center_recent_routes';
static const _kCustomRoute = 'tool_center_custom_route';
static const _maxRecentCount = 10;
static List<String> getRecentRoutes() { ... }
static Future<void> addRecentRoute(String route) { ... }
static String? getCustomRoute() { ... }
static Future<void> setCustomRoute(String route) { ... }
}
```
- [ ] **Step 2: 更新 app_nav_extension.dart — 调用 RecentRouteService 而非直接写 KvStorage**
- [ ] **Step 3: 更新 tool_center_recent_provider.dart — 从 RecentRouteService 读取**
- [ ] **Step 4: 删除 app_nav_extension.dart 中的 _recordRecentRoute 和 KvStorage 直接操作**
- [ ] **Step 5: 运行 flutter analyze 验证**
### Task P2.2: Provider 缓存同步机制
**Files:**
- Modify: `lib/features/home/providers/tool_center_recent_provider.dart`
- [ ] **Step 1: 在 ToolCenterRecentNotifier 中添加 refreshFromStorage 方法**
- [ ] **Step 2: 在面板打开时调用 ref.invalidate(toolCenterRecentProvider) 确保最新数据**
- [ ] **Step 3: 运行 flutter analyze 验证**
### Task P2.3: 删除冗余的 KvStorage 直接操作
**Files:**
- Modify: `lib/features/home/providers/tool_center_recent_provider.dart`
- [ ] **Step 1: 将 provider 内部的 KvStorage 操作全部迁移到 RecentRouteService**
- [ ] **Step 2: 运行 flutter analyze 验证**
### Task P2.4: 提交 P2 阶段代码
- [ ] **Step 1: 运行 flutter analyze 验证零 error**
- [ ] **Step 2: 更新 CHANGELOG.md**
---
## P3: 主题适配全量修复
### Task P3.1: 扩展 AppThemeExtension — 新增缺失的语义色
**Files:**
- Modify: `lib/core/theme/app_theme.dart`
- Modify: `lib/core/theme/app_colors.dart`
- [ ] **Step 1: 在 AppThemeExtension 新增属性**
```dart
final Color iconTintBlue; // 0xFF007AFF
final Color iconTintPurple; // 0xFF5856D6
final Color iconTintViolet; // 0xFFAF52DE
final Color iconTintCyan; // 0xFF5AC8FA
final Color iconTintMint; // 0xFF30D158
final Color iconTintYellow; // 0xFFFFCC00
final Color iconTintGrey; // 0xFF8E8E93
```
- [ ] **Step 2: 在 light/dark 主题中定义这些颜色值**
- [ ] **Step 3: 运行 flutter analyze 验证**
### Task P3.2: 修复高优先级硬编码色 — CupertinoColors.systemBackground + label
**Files:**
- Modify: ~29 个文件 (16处 systemBackground + 23处 label)
- [ ] **Step 1: 批量替换 CupertinoColors.systemBackground → ext.bgPrimary**
- [ ] **Step 2: 批量替换 CupertinoColors.label → ext.textPrimary**
- [ ] **Step 3: 运行 flutter analyze 验证**
### Task P3.3: 修复高优先级硬编码色 — CupertinoColors.white 在按钮文字上
**Files:**
- Modify: ~80 个文件
- [ ] **Step 1: 在强调色按钮/图标上的 CupertinoColors.white → ext.textOnAccent**
- [ ] **Step 2: 在深色背景上的 CupertinoColors.white → ext.textInverse**
- [ ] **Step 3: 运行 flutter analyze 验证**
### Task P3.4: 修复中优先级硬编码色 — Colors.black 遮罩
**Files:**
- Modify: ~39 个文件
- [ ] **Step 1: Colors.black.withValues(alpha: 0.04-0.1) → ext.overlaySubtle**
- [ ] **Step 2: Colors.black.withValues(alpha: 0.1-0.4) / Colors.black54 → ext.overlayMedium**
- [ ] **Step 3: Colors.black.withValues(alpha: 0.4+) / Colors.black87 → ext.overlayStrong**
- [ ] **Step 4: 运行 flutter analyze 验证**
### Task P3.5: 修复中优先级硬编码色 — Color(0xFF...) 十六进制色
**Files:**
- Modify: ~100 个文件
- [ ] **Step 1: Color(0xFF007AFF) → ext.accent / ext.infoColor**
- [ ] **Step 2: Color(0xFFFF3B30) → ext.errorColor / ext.destructiveColor**
- [ ] **Step 3: Color(0xFF34C759) → ext.successColor**
- [ ] **Step 4: Color(0xFFFF9500) → ext.warningColor**
- [ ] **Step 5: Color(0xFF6C63FF) → ext.accent**
- [ ] **Step 6: Color(0xFF8E8E93) → ext.iconSecondary**
- [ ] **Step 7: 其他十六进制色 → 对应 ext.xxx 属性**
- [ ] **Step 8: 运行 flutter analyze 验证**
### Task P3.6: 提交 P3 阶段代码
- [ ] **Step 1: 全量 flutter analyze**
- [ ] **Step 2: 更新 CHANGELOG.md**
---
## P4: 组件拆分
### Task P4.1: 拆分 home_daily_card.dart (841行)
**Files:**
- Create: `lib/features/home/presentation/widgets/daily_card_swiper.dart`
- Create: `lib/features/home/presentation/widgets/daily_card_actions.dart`
- Create: `lib/features/home/presentation/widgets/daily_card_loading.dart`
- Modify: `lib/features/home/presentation/home_daily_card.dart`
- [ ] **Step 1: 提取 _buildLoadingCard → daily_card_loading.dart**
- [ ] **Step 2: 提取操作按钮行 → daily_card_actions.dart**
- [ ] **Step 3: 提取 CardSwiper 配置 → daily_card_swiper.dart**
- [ ] **Step 4: 主文件调用拆分后的组件**
- [ ] **Step 5: 运行 flutter analyze 验证**
### Task P4.2: 拆分 appbar_character_sprite.dart (1430行)
**Files:**
- Create: `lib/shared/widgets/animation/sprite_painters/sprite_head_painter.dart`
- Create: `lib/shared/widgets/animation/sprite_painters/sprite_body_painter.dart`
- Create: `lib/shared/widgets/animation/sprite_painters/sprite_eyes_painter.dart`
- Create: `lib/shared/widgets/animation/sprite_painters/sprite_accessories_painter.dart`
- Create: `lib/shared/widgets/animation/sprite_animation_controller.dart`
- Modify: `lib/shared/widgets/animation/appbar_character_sprite.dart`
- [ ] **Step 1: 提取动画控制器逻辑 → sprite_animation_controller.dart**
- [ ] **Step 2: 拆分 _AppBarCharacterPainter 的绘制方法到各子 painter**
- [ ] **Step 3: 主文件组合各子组件**
- [ ] **Step 4: 运行 flutter analyze 验证**
### Task P4.3: 拆分 learning_center_page.dart (1680行)
**Files:**
- Create: `lib/features/mine/user_center/presentation/widgets/dashboard_section.dart`
- Create: `lib/features/mine/user_center/presentation/widgets/daily_recommend_section.dart`
- Create: `lib/features/mine/user_center/presentation/widgets/learning_overview_section.dart`
- Modify: `lib/features/mine/user_center/presentation/learning_center_page.dart`
- [ ] **Step 1: 提取仪表盘区域 → dashboard_section.dart**
- [ ] **Step 2: 提取每日推荐区域 → daily_recommend_section.dart**
- [ ] **Step 3: 提取学习概览区域 → learning_overview_section.dart**
- [ ] **Step 4: 主文件组合各区域组件**
- [ ] **Step 5: 运行 flutter analyze 验证**
### Task P4.4: 拆分 home_page.dart (1153行)
**Files:**
- Create: `lib/features/home/presentation/widgets/home_appbar_section.dart`
- Create: `lib/features/home/presentation/widgets/home_feed_section.dart`
- Modify: `lib/features/home/presentation/home_page.dart`
- [ ] **Step 1: 提取 AppBar 区域 → home_appbar_section.dart**
- [ ] **Step 2: 提取 Feed 列表区域 → home_feed_section.dart**
- [ ] **Step 3: 主文件组合各区域**
- [ ] **Step 4: 运行 flutter analyze 验证**
### Task P4.5: 提交 P4 阶段代码
- [ ] **Step 1: 全量 flutter analyze**
- [ ] **Step 2: 更新 CHANGELOG.md**
---
## P5: 下拉手势 + 空状态优化
### Task P5.1: 简化下拉手势为一段式
**Files:**
- Modify: `lib/features/home/presentation/home_refresh_indicator.dart`
- [ ] **Step 1: 移除二段式逻辑,改为:下拉即刷新+弹出面板**
当前0-50% 松手刷新50%+ 松手打开面板
改为:下拉松手后同时触发刷新+弹出面板,无需精确控制距离
- [ ] **Step 2: 简化进度显示 — 只显示一个进度条**
- [ ] **Step 3: 运行 flutter analyze 验证**
### Task P5.2: 精灵角色下拉互动对话
**Files:**
- Modify: `lib/features/home/presentation/home_refresh_indicator.dart`
- [ ] **Step 1: 下拉时精灵说不同的话**
```dart
const _pullDialogs = [
'想找什么?🤔',
'往下拉~',
'松手看看~',
'马上就好!',
];
```
- [ ] **Step 2: 对话气泡动画 — 从精灵头顶弹出1.5秒后消失**
- [ ] **Step 3: 运行 flutter analyze 验证**
### Task P5.3: 空状态提示优化
**Files:**
- Modify: `lib/features/home/presentation/home_tool_center.dart`
- [ ] **Step 1: "待使用" → "打开任意页面后自动记录"**
- [ ] **Step 2: 空自定义按钮提示 → "长按设置常用工具"**
- [ ] **Step 3: 运行 flutter analyze 验证**
---
## P6: 最近打开面板增强
### Task P6.1: 面板内滑动查看更多最近页面
**Files:**
- Modify: `lib/features/home/presentation/home_tool_center.dart`
- Modify: `lib/core/services/navigation/recent_route_service.dart`
- [ ] **Step 1: 最近路由上限从3增加到10**
- [ ] **Step 2: 面板布局改为4个固定按钮行 + 可滚动的更多最近页面行**
- [ ] **Step 3: 更多行使用水平滚动的 ListView**
- [ ] **Step 4: 运行 flutter analyze 验证**
### Task P6.2: 按使用频率排序
**Files:**
- Modify: `lib/core/services/navigation/recent_route_service.dart`
- [ ] **Step 1: 存储结构增加访问次数**
```dart
class RouteRecord {
final String route;
final int accessCount;
final DateTime lastAccessed;
}
```
- [ ] **Step 2: 新增 getFrequentRoutes() 方法 — 按 accessCount 降序**
- [ ] **Step 3: 面板增加排序切换按钮(最近/最常用)**
- [ ] **Step 4: 运行 flutter analyze 验证**
### Task P6.3: 智能推荐 — 根据时间段推荐
**Files:**
- Create: `lib/core/services/navigation/smart_recommend_service.dart`
- Modify: `lib/features/home/presentation/home_tool_center.dart`
- [ ] **Step 1: 创建 SmartRecommendService**
```dart
class SmartRecommendService {
static String? getRecommendedRoute() {
final hour = DateTime.now().hour;
if (hour >= 6 && hour < 9) return AppRoutes.weather;
if (hour >= 21 || hour < 1) return AppRoutes.nightRead;
if (hour >= 12 && hour < 14) return AppRoutes.pomodoro;
return null;
}
}
```
- [ ] **Step 2: 面板中显示推荐按钮(带✨标识)**
- [ ] **Step 3: 运行 flutter analyze 验证**
### Task P6.4: 面板内搜索页面
**Files:**
- Modify: `lib/features/home/presentation/home_tool_center.dart`
- [ ] **Step 1: 面板顶部增加搜索栏**
- [ ] **Step 2: 搜索结果从 PageRegistry 中匹配**
- [ ] **Step 3: 点击搜索结果跳转对应页面**
- [ ] **Step 4: 运行 flutter analyze 验证**
### Task P6.5: 拖拽排序自定义按钮
**Files:**
- Modify: `lib/features/home/presentation/home_tool_center.dart`
- [ ] **Step 1: 自定义按钮区域支持长按拖拽排序**
- [ ] **Step 2: 拖拽排序结果持久化到 KvStorage**
- [ ] **Step 3: 运行 flutter analyze 验证**
---
## P7: 标签/精灵/引导页增强
### Task P7.1: 句子广场标签 — 频道订阅/取消
**Files:**
- Create: `lib/features/home/providers/channel_subscription_provider.dart`
- Modify: `lib/features/home/presentation/home_type_chip.dart`
- Modify: `lib/features/home/presentation/home_square_header.dart`
- [ ] **Step 1: 创建频道订阅 Provider — 管理用户订阅的频道列表**
- [ ] **Step 2: 标签栏只显示已订阅频道 + "管理"按钮**
- [ ] **Step 3: 管理页面支持订阅/取消频道**
- [ ] **Step 4: 运行 flutter analyze 验证**
### Task P7.2: 句子广场标签 — 自定义频道排序
**Files:**
- Modify: `lib/features/home/presentation/home_type_chip.dart`
- [ ] **Step 1: 长按标签进入排序模式**
- [ ] **Step 2: 使用 ReorderableListView 实现拖拽排序**
- [ ] **Step 3: 排序结果持久化**
- [ ] **Step 4: 运行 flutter analyze 验证**
### Task P7.3: 拾光精灵 — 情绪系统升级
**Files:**
- Modify: `lib/features/home/providers/character_mood_provider.dart`
- [ ] **Step 1: 情绪计算增加阅读时长/频率因子**
```dart
CharacterMood calculateMood({
required int readingMinutesToday,
required int streakDays,
required int sentencesReadToday,
})
```
- [ ] **Step 2: 情绪随时间自然衰减(长时间不阅读→无聊→困倦)**
- [ ] **Step 3: 运行 flutter analyze 验证**
### Task P7.4: 拾光精灵 — 对话气泡
**Files:**
- Create: `lib/shared/widgets/animation/sprite_dialog_bubble.dart`
- Modify: `lib/features/home/presentation/home_refresh_indicator.dart`
- [ ] **Step 1: 创建 SpriteDialogBubble 组件 — 毛玻璃气泡+文字+自动消失**
- [ ] **Step 2: 下拉时在精灵上方显示对话气泡**
- [ ] **Step 3: 对话内容根据下拉进度变化**
- [ ] **Step 4: 运行 flutter analyze 验证**
### Task P7.5: 引导页 — 渐进式引导
**Files:**
- Create: `lib/features/onboarding/providers/progressive_guide_provider.dart`
- Create: `lib/shared/widgets/feedback/feature_guide_overlay.dart`
- [ ] **Step 1: 创建 ProgressiveGuideProvider — 追踪用户已看过的功能引导**
- [ ] **Step 2: 创建 FeatureGuideOverlay — 高亮某功能区域+说明文字**
- [ ] **Step 3: 首次使用某功能时弹出引导**
- [ ] **Step 4: 运行 flutter analyze 验证**
### Task P7.6: 提交 P7 阶段代码 + 更新 CHANGELOG
- [ ] **Step 1: 全量 flutter analyze**
- [ ] **Step 2: 更新 CHANGELOG.md**
- [ ] **Step 3: 清理 spec 文档**
---
## 执行顺序建议
```
P1 (类型安全) → P2 (状态管理) → P3 (主题适配) → P4 (组件拆分)
P5 (下拉手势) → P6 (最近面板) → P7 (标签/精灵/引导页)
```
P1-P4 是架构基础必须先完成。P5-P7 是功能增强,依赖 P1-P4 的基础。

View File

@@ -1,143 +0,0 @@
# 架构重构与功能增强 实施计划
> **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:** 修复9项架构不足动态圆角生效、角色精灵主题化、枚举位置迁移、引导页解耦、设置Provider简化、互斥锁超时、渐变背景优化、个性化页增强、协议页增强
**Architecture:** 逐项独立修改,每项完成后验证编译通过。动态圆角采用"先写测试→再批量替换"策略确保安全。
**Tech Stack:** Flutter/Dart, Riverpod, CustomPainter, ThemeExtension
---
## Task 1: CharacterExpression 枚举迁移(独立,无依赖)
**Files:**
- Create: `lib/core/constants/character_expression.dart`
- Modify: `lib/shared/widgets/animation/appbar_character_sprite.dart`
- Modify: `lib/features/onboarding/presentation/onboarding_page.dart`
- Modify: `lib/features/home/presentation/home_page.dart`
- Modify: `lib/features/home/presentation/home_refresh_indicator.dart`
- Modify: `lib/shared/widgets/animation/sprite_loading_indicator.dart`
- [ ] **Step 1:** 创建 `lib/core/constants/character_expression.dart`,将枚举从 `appbar_character_sprite.dart` 提取出来
- [ ] **Step 2:** 修改 `appbar_character_sprite.dart`,删除枚举定义,改为 `import`
- [ ] **Step 3:** 修改其他4个引用文件将 import 路径从 `appbar_character_sprite.dart` 改为 `character_expression.dart`
- [ ] **Step 4:** 运行 `flutter analyze` 验证
---
## Task 2: EffectMutex 超时机制(独立)
**Files:**
- Modify: `lib/core/services/performance/effect_mutex.dart`
- [ ] **Step 1:**`acquire()` 添加 `Duration? timeout` 参数
- [ ] **Step 2:** 当 timeout 不为 null 时,用 `Future.any` + `Future.delayed` 实现超时取消
- [ ] **Step 3:** 超时后从 `_pending` 列表移除complete 一个异常或 null token
- [ ] **Step 4:** 运行 `flutter analyze` 验证
---
## Task 3: Onboarding 完成回调解耦(独立)
**Files:**
- Modify: `lib/features/onboarding/providers/onboarding_provider.dart`
- [ ] **Step 1:**`completeOnboarding()` 中,将直接调用 `generalSettingsProvider.notifier` 的3行替换为直接写 KV 存储(与 generalSettingsProvider 使用相同的 key让 generalSettingsProvider 在下次 build 时自动从 KV 读取
- [ ] **Step 2:** 确认 KV key 一致性(`general_shake_to_switch` / `sfx_enabled` / `general_shader_background`
- [ ] **Step 3:** 运行 `flutter analyze` 验证
---
## Task 4: 角色精灵硬编码颜色主题化(独立,大改动)
**Files:**
- Modify: `lib/shared/widgets/animation/appbar_character_sprite.dart`
- Modify: `lib/core/theme/app_theme.dart`(新增角色配色令牌)
- [ ] **Step 1:**`AppThemeExtension` 中新增角色配色字段:`characterSkinLight`/`characterSkinMid`/`characterSkinDark`/`characterEarInner`/`characterNose`/`characterHair`/`characterOutline`
- [ ] **Step 2:**`_lightExtension`/`_darkExtension`/`_amoledExtension` 中定义浅色/深色/AMOLED三套角色配色
- [ ] **Step 3:** 修改 `_AppBarCharacterPainter`,将所有硬编码颜色替换为主题令牌参数
- [ ] **Step 4:** 修改 `AppBarCharacterSprite.build()` 传递主题颜色到 Painter
- [ ] **Step 5:** 运行 `flutter analyze` 验证
---
## Task 5: 动态圆角批量替换(高风险,需测试先行)
**Files:**
- Modify: 100+ 文件602处 `AppRadius.xxxBorder``AppRadius.of(context).xxxBorder`
- Create: `test/core/theme/app_radius_test.dart`
- [ ] **Step 1:** 编写测试文件 `app_radius_test.dart`,验证 `AppRadiusData._fromId` 的4种风格返回正确值
- [ ] **Step 2:** 运行测试确认通过
- [ ] **Step 3:** 用脚本批量替换:`AppRadius.xsBorder``AppRadius.of(context).xsBorder` 等6种模式
- [ ] **Step 4:** 手动检查替换结果,修复无 context 的场景(如 Provider/常量定义处)
- [ ] **Step 5:** 运行 `flutter analyze` 验证全项目无错误
- [ ] **Step 6:** 运行测试确认通过
---
## Task 6: general_settings_provider.dart 简化(独立)
**Files:**
- Modify: `lib/features/mine/settings/providers/general_settings_provider.dart`
- [ ] **Step 1:** 提取通用 `_setField<T>` 方法,封装"更新state + 写KV"模式
- [ ] **Step 2:** 将所有 setter 方法改为调用 `_setField`
- [ ] **Step 3:** 运行 `flutter analyze` 验证
---
## Task 7: Mesh渐变背景优化独立
**Files:**
- Modify: `lib/features/onboarding/presentation/widgets/mesh_gradient_background.dart`
- [ ] **Step 1:** 预计算色块路径:在 `_initBlobs` 中生成 `List<Offset>` 路径点,动画时只做插值
- [ ] **Step 2:** 扩展 `_isLowEnd` 判断iOS 检测 `Platform.isIOS` + 低内存设备列表Android 检测 `DeviceInfoPlugin` 低 RAM
- [ ] **Step 3:** 运行 `flutter analyze` 验证
---
## Task 8: 个性化页增强(依赖 Task 4 角色主题化)
**Files:**
- Modify: `lib/features/onboarding/presentation/pages/personalization_page.dart`
- [ ] **Step 1:** 预览卡片增强:实时反映主题模式(浅/深色切换时卡片背景变化)和强调色变化
- [ ] **Step 2:** 增加圆角风格选择3个选项紧凑/标准/圆润),使用 CupertinoIcons
- [ ] **Step 3:** 运行 `flutter analyze` 验证
---
## Task 9: 协议页章节导航 + 权限分组(独立)
**Files:**
- Modify: `lib/features/onboarding/presentation/pages/agreement_page.dart`
- Modify: `lib/core/services/auth/permission_service.dart`(添加分组字段)
- [ ] **Step 1:**`AppPermission` 枚举添加 `group` 字段(必要权限/可选权限/系统权限)
- [ ] **Step 2:** 修改权限列表展示,按分组显示,每组有标题
- [ ] **Step 3:** 协议内容添加章节锚点导航(顶部章节标签)
- [ ] **Step 4:** 运行 `flutter analyze` 验证
---
## 执行顺序
```
Task 1 (枚举迁移) ─── 独立,可先执行
Task 2 (互斥锁超时) ── 独立,可先执行
Task 3 (引导页解耦) ── 独立,可先执行
Task 4 (角色主题化) ── 独立,但 Task 8 依赖它
Task 5 (动态圆角) ──── 高风险,需测试先行
Task 6 (设置简化) ──── 独立
Task 7 (渐变优化) ──── 独立
Task 8 (个性化增强) ── 依赖 Task 4
Task 9 (协议页增强) ── 独立
```
可并行Task 1/2/3/6/7/9 可同时执行
串行Task 4 → Task 8Task 5 需单独执行(影响面太大)

File diff suppressed because it is too large Load Diff

View File

@@ -1,528 +0,0 @@
# 宽屏适配增强 — 22项任务实施计划
> **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:** 实施宽屏适配的22项增强任务涵盖架构优化、UX改进、动画升级、库集成和高级功能
**Architecture:** 以 splitViewProvider 为唯一数据源,消除双写;使用 flutter_animate 替代手动 AnimationController引入 freezed 生成不可变状态类;构建三栏布局支持超宽屏
**Tech Stack:** Flutter 3.11+ / Riverpod 3.0 / flutter_animate 4.5 / freezed 3.2 / photo_view 0.15 / heroine 0.7 / desktop_drop / bitsdojo_window
---
## 文件结构
### 新建文件
- `lib/core/providers/split_view_provider.freezed.dart` — freezed 生成
- `lib/core/layout/panel_cache.dart` — 面板缓存管理
- `lib/core/layout/smart_app_bar.dart` — 智能AppBar位置适配
- `lib/core/layout/panel_bookmark.dart` — 面板书签功能
- `lib/core/layout/triple_column_view.dart` — 三栏布局组件
### 修改文件
- `lib/core/providers/split_view_provider.dart` — freezed重写+三栏状态
- `lib/core/layout/adaptive_split_view.dart` — flutter_animate+三栏
- `lib/core/layout/split_divider.dart` — 双击重置+触控区域增大
- `lib/core/layout/overview_dashboard.dart` — 接入真实Provider数据
- `lib/core/layout/app_shell.dart` — 横屏居中+三栏+智能AppBar
- `lib/core/layout/adaptive_nav_bar.dart` — 自定义标题栏融合
- `lib/features/discover/presentation/panels/chat_flow_panel.dart` — 键盘适配
- `lib/features/home/presentation/home_page.dart` — Hero动画
- `lib/features/home/presentation/panels/sentence_detail_panel.dart` — photo_view+Hero
- `lib/features/mine/settings/providers/sub/display_settings_provider.dart` — 移除分屏字段
- `lib/features/mine/settings/providers/general_settings_provider.dart` — 移除分屏setter
- `lib/features/mine/settings/presentation/general/general_settings_sections.dart` — 设置项读取splitViewProvider
- `lib/features/mine/settings/presentation/general/general_settings_pickers.dart` — picker读取splitViewProvider
- `pubspec.yaml` — 新增依赖
---
## Phase 1: 核心架构修复
### Task 1: 状态管理去重 — splitViewProvider 为唯一数据源
**Files:**
- Modify: `lib/core/providers/split_view_provider.dart`
- Modify: `lib/features/mine/settings/providers/sub/display_settings_provider.dart`
- Modify: `lib/features/mine/settings/providers/general_settings_provider.dart`
- Modify: `lib/features/mine/settings/presentation/general/general_settings_sections.dart`
- Modify: `lib/features/mine/settings/presentation/general/general_settings_pickers.dart`
- Modify: `lib/features/mine/settings/presentation/general/general_settings_page.dart`
- [ ] **Step 1: 从 DisplaySettingsState 移除分屏三字段**
`display_settings_provider.dart` 中移除 `navBarPositionIndex``splitRatio``splitViewEnabled` 三个字段及其 copyWith 参数、fromStorage/saveToStorage 中的对应行、setter 方法。
- [ ] **Step 2: 从 GeneralSettingsState 移除分屏相关 getter 和 setter**
`general_settings_provider.dart` 中移除 `navBarPositionIndex``splitRatio``splitViewEnabled` 三个 getter 和 `setNavBarPosition``setSplitRatio``setSplitViewEnabled` 三个 setter。
- [ ] **Step 3: 修改设置项数据源为 splitViewProvider**
`general_settings_sections.dart` 中,将 `settings.navBarPositionIndex`/`settings.splitRatio`/`settings.splitViewEnabled` 改为接收外部参数,调用方从 `ref.watch(splitViewProvider)` 读取。修改 `buildGeneralSettingSections` 签名增加 `SplitViewState splitState` 参数。
- [ ] **Step 4: 修改 picker 直接操作 splitViewProvider**
`general_settings_pickers.dart` 中,`showNavBarPositionPicker`/`showSplitRatioPicker` 移除对 `generalSettingsProvider` 的写入,仅操作 `splitViewProvider`
- [ ] **Step 5: 修改 toggle 事件直接操作 splitViewProvider**
`general_settings_page.dart` 中,`_onToggle``split_view_enabled` case 移除对 `notifier.setSplitViewEnabled(value)` 的调用,仅保留 `ref.read(splitViewProvider.notifier).setSplitViewEnabled(value)`
- [ ] **Step 6: 验证编译通过**
Run: `flutter analyze --no-fatal-infos`
Expected: 零 error
### Task 2: 面板内容缓存 — AutomaticKeepAliveClientMixin
**Files:**
- Modify: `lib/core/layout/adaptive_split_view.dart`
- Create: `lib/core/layout/panel_cache.dart`
- [ ] **Step 1: 创建 PanelCacheWrapper 组件**
创建 `panel_cache.dart`,实现一个 `KeepAlivePanelWrapper`,使用 `AutomaticKeepAliveClientMixin` 保持子组件存活:
```dart
class KeepAlivePanelWrapper extends ConsumerStatefulWidget {
const KeepAlivePanelWrapper({required this.child, super.key});
final Widget child;
@override
ConsumerState<KeepAlivePanelWrapper> createState() => _KeepAlivePanelWrapperState();
}
class _KeepAlivePanelWrapperState extends ConsumerState<KeepAlivePanelWrapper>
with AutomaticKeepAliveClientMixin {
@override
bool get wantKeepAlive => true;
@override
Widget build(BuildContext context) {
super.build(context);
return widget.child;
}
}
```
- [ ] **Step 2: 在 AdaptiveSplitView 中包裹左右面板**
修改 `adaptive_split_view.dart`,在左侧面板和右侧面板外层包裹 `KeepAlivePanelWrapper`确保切换Tab时面板不被销毁。
- [ ] **Step 3: 验证编译通过**
---
## Phase 2: UX 改进
### Task 3: 右侧面板键盘适配
**Files:**
- Modify: `lib/features/discover/presentation/panels/chat_flow_panel.dart`
- [ ] **Step 1: 在 ChatFlowPanel 中处理键盘遮挡**
`_ChatFlowPanelState.build()` 中,使用 `MediaQuery.viewInsets.bottom` 获取键盘高度,在面板底部添加对应的 padding
```dart
final keyboardHeight = MediaQuery.of(context).viewInsets.bottom;
// 在 Column 底部添加 SizedBox(height: keyboardHeight)
```
- [ ] **Step 2: 验证编译通过**
### Task 4: 分割线双击重置 + 触控区域增大
**Files:**
- Modify: `lib/core/layout/split_divider.dart`
- [ ] **Step 1: 增大触控区域至 24px**
修改外层 Container 的 width 从 17 改为 24内部手柄视觉保持细线2px但触控区域更大。
- [ ] **Step 2: 添加双击重置功能**
`GestureDetector` 外层包裹 `GestureDetector(onDoubleTap: ...)`,双击时调用 `onRatioChanged(0.4)` 重置为默认比例,并添加 `HapticFeedback.mediumImpact()` 触觉反馈。
- [ ] **Step 3: 验证编译通过**
### Task 5: 窄屏横屏居中效果
**Files:**
- Modify: `lib/core/layout/app_shell.dart`
- [ ] **Step 1: 在 _buildNarrowLayout 中添加横屏居中逻辑**
检测 `MediaQuery.of(context).size.width > 600 && MediaQuery.of(context).size.height < 500`(横屏但未达分屏断点),使用 `Center + ConstrainedBox(maxWidth: 600)` 包裹内容区域,两侧留白。
- [ ] **Step 2: 验证编译通过**
---
## Phase 3: 数据与动画
### Task 6: 概览仪表盘接入真实 Provider 数据
**Files:**
- Modify: `lib/core/layout/overview_dashboard.dart`
- [ ] **Step 1: 接入今日推荐数据**
使用 `ref.watch(homeProvider)` 获取推荐句子列表替换硬编码的3条假数据。空状态时显示占位提示。
- [ ] **Step 2: 接入快捷操作数据**
使用 `ref.watch(generalSettingsProvider)` 获取用户常用功能列表替换8项硬编码操作。
- [ ] **Step 3: 接入统计数据**
使用 `ref.watch(userStatsProvider)` 或类似 Provider 获取真实统计值收藏数、阅读天数等替换全0硬编码。
- [ ] **Step 4: 验证编译通过**
### Task 7: 面板切换 Hero 动画
**Files:**
- Modify: `lib/features/home/presentation/home_page.dart`
- Modify: `lib/features/home/presentation/panels/sentence_detail_panel.dart`
- [ ] **Step 1: 在句子卡片添加 Heroine tag**
`home_page.dart``_showDailyDetailSheet` 中,为句子卡片添加 `Heroine(tag: 'sentence_${item.id}')` 包裹。
- [ ] **Step 2: 在详情面板添加对应 Heroine tag**
`sentence_detail_panel.dart` 的句子卡片区域添加 `Heroine(tag: 'sentence_${widget.sentenceId}')` 包裹,确保窄屏 Sheet 和宽屏 Panel 共享相同的 Hero tag。
- [ ] **Step 3: 验证编译通过**
### Task 8: flutter_animate 替代手动 AnimationController
**Files:**
- Modify: `lib/core/layout/adaptive_split_view.dart`
- [ ] **Step 1: 移除手动 AnimationController**
移除 `SingleTickerProviderStateMixin``AnimationController``CurvedAnimation``SlideTransition``FadeTransition`
- [ ] **Step 2: 使用 flutter_animate 重写面板动画**
使用 `AnimateController` + `.slideX()` + `.fadeIn()` 替代,代码更简洁:
```dart
final controller = AnimateController(onAttach: (c) { /* forward/reverse logic */ });
rightPanel.animate(controller: controller).slideX(begin: 1.0, end: 0.0, duration: 350.ms, curve: Curves.easeInOutCubic).fadeIn(duration: 200.ms);
```
- [ ] **Step 3: 验证编译通过**
### Task 9: flutter_staggered_animations 列表交错动画
**Files:**
- Modify: `pubspec.yaml`
- Modify: `lib/core/layout/overview_dashboard.dart`
- [ ] **Step 1: 添加依赖**
`pubspec.yaml` 中添加 `flutter_staggered_animations: ^0.1.3`
- [ ] **Step 2: 在概览仪表盘中使用交错动画**
`_buildQuickActions``_buildStats` 中,使用 `AnimationLimiter` + `AnimationConfiguration.staggeredList` + `SlideAnimation` + `FadeInAnimation` 替代手动 stagger 逻辑。
- [ ] **Step 3: 运行 flutter pub get**
---
## Phase 4: 库集成
### Task 10: photo_view 图片查看器集成
**Files:**
- Modify: `lib/features/home/presentation/panels/sentence_detail_panel.dart`
- [ ] **Step 1: 在句子详情面板中集成 photo_view**
当句子包含图片时,点击图片使用 `PhotoView` 全屏查看,支持缩放/平移。使用 `PhotoView.gallery` 支持多图浏览。
- [ ] **Step 2: 验证编译通过**
### Task 11: dartx 集合扩展方法
**Files:**
- Modify: `pubspec.yaml`
- Modify: `lib/core/providers/split_view_provider.dart`
- Modify: `lib/core/layout/right_panel_registry.dart`
- [ ] **Step 1: 添加依赖**
`pubspec.yaml` 中添加 `dartx: ^1.2.0`
- [ ] **Step 2: 使用 dartx 简化数据处理**
`split_view_provider.dart` 中使用 `.firstOrNull``.sortedBy()` 等扩展方法简化逻辑。在 `right_panel_registry.dart` 中使用 `.getOrNull()` 等安全访问。
- [ ] **Step 3: 运行 flutter pub get**
### Task 12: freezed 不可变数据类
**Files:**
- Modify: `lib/core/providers/split_view_provider.dart`
- Create: `lib/core/providers/split_view_provider.freezed.dart`
- [ ] **Step 1: 为 SplitViewState 添加 freezed 注解**
`SplitViewState` 改为 freezed 类:
```dart
@freezed
class SplitViewState with _$SplitViewState {
const SplitViewState._();
const factory SplitViewState({
@Default(0.4) double splitRatio,
String? rightPanelContent,
Map<String, dynamic>? rightPanelArgs,
@Default(NavBarPosition.left) NavBarPosition navBarPosition,
@Default(true) bool splitViewEnabled,
String? homeRightPanel,
String? discoverRightPanel,
String? profileRightPanel,
@Default(0) int currentTab,
}) = _SplitViewState;
String? get activeRightPanel => switch (currentTab) { ... };
}
```
- [ ] **Step 2: 运行 build_runner 生成代码**
Run: `dart run build_runner build --delete-conflicting-outputs`
- [ ] **Step 3: 修复编译错误**
更新所有引用 `SplitViewState` 的地方,确保 `copyWith` 签名兼容。
- [ ] **Step 4: 验证编译通过**
---
## Phase 5: 高级功能
### Task 13: 文件拖放发送 — desktop_drop
**Files:**
- Modify: `pubspec.yaml`
- Modify: `lib/features/discover/presentation/panels/chat_flow_panel.dart`
- [ ] **Step 1: 添加依赖**
`pubspec.yaml` 中添加 `desktop_drop: ^0.5.0`
- [ ] **Step 2: 在 ChatFlowPanel 中集成拖放区域**
使用 `DropTarget` 包裹聊天面板,监听 `onDragDone` 获取拖入的文件路径,调用聊天发送逻辑。
- [ ] **Step 3: 添加拖放视觉反馈**
`onDragEntered` 时显示高亮边框,`onDragExited` 时移除。
- [ ] **Step 4: 验证编译通过**
### Task 14: 自定义标题栏 — bitsdojo_window
**Files:**
- Modify: `pubspec.yaml`
- Modify: `lib/core/layout/app_shell.dart`
- Modify: `lib/core/layout/adaptive_nav_bar.dart`
- Modify: `lib/main.dart`
- [ ] **Step 1: 添加依赖**
`pubspec.yaml` 中添加 `bitsdojo_window: ^0.1.6`
- [ ] **Step 2: 在 main.dart 中初始化**
`main()` 中添加 `BitsdojoWindow.doWhenWindowReady()` 设置最小窗口尺寸和标题栏高度。
- [ ] **Step 3: 在 AdaptiveNavBar 中融合标题栏**
当导航栏位置为顶部时,将窗口控制按钮(关闭/最小化/最大化)嵌入导航栏右侧,使用 `WindowTitleBarBox` + `MoveWindow` 实现拖拽移动。
- [ ] **Step 4: 在 AppShell 中处理标题栏区域**
宽屏模式下移除系统标题栏,使用 `appWindow.titleBarHeight = 0`,导航栏融合标题栏功能。
- [ ] **Step 5: 验证编译通过(仅桌面端生效,移动端无影响)**
### Task 15: 面板书签功能
**Files:**
- Create: `lib/core/layout/panel_bookmark.dart`
- Modify: `lib/core/providers/split_view_provider.dart`
- [ ] **Step 1: 创建 PanelBookmark 数据模型和 Provider**
```dart
class PanelBookmark {
const PanelBookmark({required this.name, required this.tabIndex, required this.panelId, this.panelArgs});
final String name;
final int tabIndex;
final String panelId;
final Map<String, dynamic>? panelArgs;
}
```
使用 Hive 持久化书签列表。
- [ ] **Step 2: 在 SplitViewNotifier 中添加书签方法**
添加 `addBookmark()``removeBookmark()``applyBookmark()` 方法。
- [ ] **Step 3: 在概览仪表盘中显示书签快捷入口**
在 OverviewDashboard 中添加"已保存的面板组合"区域,点击可一键切换。
- [ ] **Step 4: 验证编译通过**
### Task 16: 跨面板拖拽 — LongPressDraggable
**Files:**
- Modify: `lib/features/home/presentation/home_page.dart`
- Modify: `lib/features/home/presentation/panels/sentence_detail_panel.dart`
- [ ] **Step 1: 在句子卡片添加 LongPressDraggable**
在 HomeSentenceCard 外层包裹 `LongPressDraggable<String>`data 为句子ID。
- [ ] **Step 2: 在右侧面板添加 DragTarget**
在 SentenceDetailPanel 外层包裹 `DragTarget<String>`接收拖入的句子ID自动加载对应详情。
- [ ] **Step 3: 添加拖拽视觉反馈**
拖拽中显示半透明卡片预览,目标区域高亮。
- [ ] **Step 4: 验证编译通过**
### Task 17: 三栏布局 — 超宽屏支持
**Files:**
- Create: `lib/core/layout/triple_column_view.dart`
- Modify: `lib/core/providers/split_view_provider.dart`
- Modify: `lib/core/layout/app_shell.dart`
- [ ] **Step 1: 在 SplitViewState 中添加三栏状态**
添加 `thirdPanelContent``thirdPanelArgs``thirdPanelRatio` 字段和对应 setter。添加 `kTripleColumnBreakpoint = 1400.0` 常量。
- [ ] **Step 2: 创建 TripleColumnView 组件**
实现三栏布局:左侧列表 + 中间详情 + 右侧辅助面板。两条可拖拽分割线。
- [ ] **Step 3: 在 AppShell 中集成三栏检测**
当屏幕宽度 >= 1400px 时,使用 TripleColumnView 替代 AdaptiveSplitView。右侧面板可再分屏。
- [ ] **Step 4: 验证编译通过**
---
## Phase 6: 平台与打磨
### Task 18: rive 交互式动画
**Files:**
- Modify: `pubspec.yaml`
- Modify: `lib/core/layout/adaptive_nav_bar.dart`
- [ ] **Step 1: 添加依赖**
`pubspec.yaml` 中添加 `rive: ^0.13.0`
- [ ] **Step 2: 创建 Rive Tab 动画组件**
创建 `RiveTabIcon` 组件,加载 Rive 动画文件根据选中状态切换动画状态机输入isSelected → bounce/idle
- [ ] **Step 3: 在 AdaptiveNavBar 中可选替换 TabIconSprite**
当用户设置启用 Rive 动画时,使用 `RiveTabIcon` 替代 `TabIconSprite`
- [ ] **Step 4: 验证编译通过**
### Task 19: 跨平台兼容处理
**Files:**
- Modify: `lib/core/layout/app_shell.dart`
- Modify: `lib/core/layout/adaptive_nav_bar.dart`
- Modify: `lib/core/layout/split_divider.dart`
- [ ] **Step 1: 平台条件编译处理**
`desktop_drop``bitsdojo_window` 等桌面端库使用 `kIsWeb` / `Platform.isWindows` 等条件判断,移动端跳过桌面特有逻辑。
- [ ] **Step 2: 触觉反馈平台适配**
`split_divider.dart` 中的 `HapticFeedback` 在桌面端无效,添加平台检测跳过。
- [ ] **Step 3: 键盘快捷键支持**
桌面端添加 `Shortcuts` + `Actions`,支持 Ctrl+1/2/3 切换TabCtrl+W 关闭面板。
- [ ] **Step 4: 验证全平台编译通过**
### Task 20: 智能 AppBar 标题位置适配
**Files:**
- Create: `lib/core/layout/smart_app_bar.dart`
- Modify: `lib/core/layout/app_shell.dart`
- [ ] **Step 1: 创建 SmartAppBar 组件**
自动检测当前是否为宽屏分屏模式:
- 窄屏:标题在 AppBar 中居中
- 宽屏左侧面板:标题在内容区靠左(无 AppBar
- 宽屏右侧面板:标题在面板 Header 靠左
```dart
class SmartAppBar extends ConsumerWidget {
const SmartAppBar({required this.title, this.actions, super.key});
final String title;
final List<Widget>? actions;
@override
Widget build(BuildContext context, WidgetRef ref) {
final isWidescreen = MediaQuery.of(context).size.width >= kSplitViewBreakpoint;
if (isWidescreen) {
return _buildContentHeader(context);
}
return _buildCupertinoAppBar(context);
}
}
```
- [ ] **Step 2: 在 AppShell 中集成 SmartAppBar**
替换各面板中的硬编码 AppBar使用 SmartAppBar 自动适配位置。
- [ ] **Step 3: 验证编译通过**
---
## Phase 7: 最终验收审计
### Task 21: 全量编译验证 + 归档文档更新
- [ ] **Step 1: 运行 flutter analyze**
Run: `flutter analyze --no-fatal-infos`
Expected: 零 error
- [ ] **Step 2: 更新归档文档**
更新 `docs/specs/2026-05-29-widescreen-adaptation-archive.md`,添加新增的视图组件、逻辑事件和动画。
- [ ] **Step 3: 更新 CHANGELOG.md**
添加 v6.5.99 版本记录包含所有22项任务的变更。
- [ ] **Step 4: 逐项审计验收**
对照22项需求逐项验证功能是否正确实现。