# 字体管理页面重构与功能增强 实施计划 > **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:** 重构字体管理页面,修复核心缺陷(自定义字体全局生效/序列化脆弱/鸿蒙端不可用),并增强交互体验(预览大图/滑动删除/搜索筛选/动画/收藏等),使字体管理达到 iOS 26 风格的精致水准。 **Architecture:** 基于 Riverpod 状态管理 + AppThemeExtension 动态主题 + 项目统一组件库(AppSlidable/AppBottomSheet/SkeletonBox/AppIcon)。数据层使用 Hive(AppKVStore) + JSON 序列化替代脆弱的 `|` 分隔符。鸿蒙端通过条件导入 + path_provider_ohos + file_picker_ohos 实现适配。 **Tech Stack:** Flutter 3.27+ / Riverpod 3.x / Hive / Supabase(在线字体数据) / google_fonts / flutter_slidable / stupid_simple_sheet / flutter_animate / heroine / shimmer / custom_refresh_indicator / archive / share_plus / pinyin / flutter_svg --- ## 文件结构 ### 新建文件 | 文件 | 职责 | |------|------| | `lib/features/settings/presentation/font/font_preview_sheet.dart` | 字体预览大图 Sheet(半屏面板+自定义预览文本+字号滑块) | | `lib/features/settings/presentation/font/font_search_bar.dart` | 字体搜索栏组件(搜索+筛选+拼音匹配) | | `lib/features/settings/presentation/font/font_comparison_page.dart` | 字体对比模式页面(左右分屏对比两种字体) | | `lib/features/settings/services/font_sync_service.dart` | 在线字体数据同步服务(Supabase 远程字体列表) | | `assets/svgs/font_import.svg` | 字体导入图标 | | `assets/svgs/font_url.svg` | URL下载图标 | | `assets/svgs/font_online.svg` | 在线字体图标 | | `assets/svgs/font_preview.svg` | 字体预览图标 | | `assets/svgs/font_compare.svg` | 字体对比图标 | | `assets/svgs/font_favorite.svg` | 字体收藏图标 | | `assets/svgs/font_zip.svg` | ZIP导入图标 | ### 修改文件 | 文件 | 修改内容 | |------|---------| | `lib/features/settings/presentation/font_models.dart` | FontInfo 增加 author/license/category/tags/isFavorite/installedAt/sourceUrl/thumbnailUrl 字段 + toJson/fromJson + FontManagementState 增加 searchQuery/filterCategory/sortBy/downloadQueue/error 字段 | | `lib/features/settings/presentation/font_management_notifier.dart` | 修复 _applyCustomFont / JSON序列化 / 鸿蒙端适配 / 并发下载控制 / 下载重试 / 内置字体映射动态化 / 批量删除 / ZIP导入 / 收藏持久化 | | `lib/features/settings/presentation/font_widgets.dart` | AppSlidable滑动删除 / 预览大图Sheet / 搜索栏 / 骨架屏 / 入场动画 / Hero动画 / 下拉刷新 / 下载进度圆环 / emoji→icon替换 / ScrollController滚动定位 | | `lib/features/settings/presentation/font_management_page.dart` | 改为 StatefulWidget + ScrollController + 下拉刷新 + 搜索栏集成 | | `lib/features/settings/providers/theme_settings_provider.dart` | ThemeSettingsState 增加 customFontFamily 字段 + setCustomFontFamily 方法 | | `lib/core/theme/app_theme.dart` | buildFromSettings 使用 customFontFamily | | `lib/app/app.dart` | effectiveFontFamily 逻辑适配 customFontFamily | | `lib/shared/widgets/app_icon.dart` | AppIconData 增加字体相关 SVG 常量 | | `pubspec.yaml` | assets/svgs/ 下新增 SVG 文件声明(如需) | | `CHANGELOG.md` | 版本记录 | --- ## Phase 1: 核心缺陷修复(必须先完成) ### Task 1: 修复 _applyCustomFont — 自定义字体全局生效 **Files:** - Modify: `lib/features/settings/providers/theme_settings_provider.dart` - Modify: `lib/features/settings/presentation/font_management_notifier.dart` - Modify: `lib/app/app.dart` - [ ] **Step 1: ThemeSettingsState 增加 customFontFamily 字段** 在 `theme_settings_provider.dart` 的 `ThemeSettingsState` 中: ```dart class ThemeSettingsState { const ThemeSettingsState({ // ... 现有字段 this.customFontFamily = '', // 空字符串表示无自定义字体 }); final String customFontFamily; // ... copyWith 中也增加 customFontFamily } ``` 在 `ThemeSettingsNotifier` 中增加方法: ```dart void setCustomFontFamily(String fontFamily) { state = state.copyWith(customFontFamily: fontFamily); AppKVStore.setString('${_keyPrefix}custom_font_family', fontFamily); } ``` 在 `_initState()` 加载中增加: ```dart final customFontFamily = AppKVStore.getString('${_keyPrefix}custom_font_family') ?? ''; ``` - [ ] **Step 2: 修复 font_management_notifier.dart 中的 _applyCustomFont** ```dart void _applyCustomFont(String fontFamily) { ref.read(themeSettingsProvider.notifier).setCustomFontFamily(fontFamily); Log.i('自定义字体已应用: $fontFamily (通过 customFontFamily 传递给主题系统)'); } ``` 同时修复 `setActiveFont` 中内置字体切换时清除 customFontFamily: ```dart void setActiveFont(String fontFamily) async { // ... if (isBuiltIn) { ref.read(themeSettingsProvider.notifier).setCustomFontFamily(''); final fontStyle = _builtInFonts.firstWhere( (f) => f.fontFamily == fontFamily, orElse: () => const FontInfo(name: '', fontFamily: 'Inter', path: '', isBuiltIn: true), ); final id = _builtInFontStyleMap[fontStyle.fontFamily] ?? 'system'; ref.read(themeSettingsProvider.notifier).setFontStyle(id); } else { // ... 加载字体到引擎 _applyCustomFont(fontFamily); } } ``` - [ ] **Step 3: 修改 app.dart 中的 effectiveFontFamily 逻辑** ```dart final effectiveFontFamily = settings.customFontFamily.isNotEmpty ? settings.customFontFamily : (!builtInFontFamilies.contains(fontState.activeFontFamily) ? fontState.activeFontFamily : settings.fontStyle.fontFamily); ``` - [ ] **Step 4: 运行分析验证** Run: `.\scripts\analyze.ps1` Expected: No issues found - [ ] **Step 5: Commit** ```bash git add -A && git commit -m "fix: 自定义字体通过 customFontFamily 传递给全局主题系统,确保全局生效" ``` --- ### Task 2: 序列化改用 JSON — 修复脆弱的 | 分隔符 **Files:** - Modify: `lib/features/settings/presentation/font_models.dart` - Modify: `lib/features/settings/presentation/font_management_notifier.dart` - [ ] **Step 1: FontInfo 增加 toJson/fromJson** 在 `font_models.dart` 的 `FontInfo` 中: ```dart Map toJson() => { 'name': name, 'fontFamily': fontFamily, 'path': path, 'isBuiltIn': isBuiltIn, 'isDownloaded': isDownloaded, 'fileSize': fileSize, 'author': author, 'license': license, 'category': category, 'isFavorite': isFavorite, 'installedAt': installedAt?.millisecondsSinceEpoch, 'sourceUrl': sourceUrl, }; factory FontInfo.fromJson(Map json) => FontInfo( name: json['name'] as String? ?? '', fontFamily: json['fontFamily'] as String? ?? '', path: json['path'] as String? ?? '', isBuiltIn: json['isBuiltIn'] as bool? ?? false, isDownloaded: json['isDownloaded'] as bool? ?? false, fileSize: json['fileSize'] as int?, author: json['author'] as String?, license: json['license'] as String?, category: json['category'] as String?, isFavorite: json['isFavorite'] as bool? ?? false, installedAt: json['installedAt'] != null ? DateTime.fromMillisecondsSinceEpoch(json['installedAt'] as int) : null, sourceUrl: json['sourceUrl'] as String?, ); ``` - [ ] **Step 2: 修改 Notifier 中的序列化/反序列化** ```dart List _loadInstalledFontsFromKV() { final raw = AppKVStore.getString(_kvKeyInstalledFonts); if (raw == null || raw.isEmpty) return []; try { final list = jsonDecode(raw) as List; return list.map((e) => FontInfo.fromJson(e as Map)).toList(); } catch (e) { Log.e('字体列表解析失败,尝试旧格式迁移', e); return _migrateFromOldFormat(); } } void _saveInstalledFontsToKV(List fonts) { final json = jsonEncode(fonts.map((f) => f.toJson()).toList()); AppKVStore.setString(_kvKeyInstalledFonts, json); } List _migrateFromOldFormat() { final raw = AppKVStore.getStringList(_kvKeyInstalledFonts) ?? []; final fonts = raw.map((entry) { final parts = entry.split('|'); if (parts.length >= 3) { return FontInfo( name: parts[0], fontFamily: parts[1], path: parts[2], isDownloaded: true, fileSize: parts.length >= 4 ? int.tryParse(parts[3]) : null, ); } return null; }).whereType().toList(); if (fonts.isNotEmpty) _saveInstalledFontsToKV(fonts); return fonts; } ``` - [ ] **Step 3: 运行分析验证** Run: `.\scripts\analyze.ps1` Expected: No issues found - [ ] **Step 4: Commit** ```bash git add -A && git commit -m "fix: 字体序列化改用JSON,兼容旧|分隔符格式自动迁移" ``` --- ### Task 3: 内置字体映射动态化 **Files:** - Modify: `lib/features/settings/presentation/font_models.dart` - Modify: `lib/features/settings/presentation/font_management_notifier.dart` - [ ] **Step 1: 在 font_models.dart 中定义内置字体配置** ```dart class BuiltInFontConfig { const BuiltInFontConfig({ required this.name, required this.fontFamily, required this.styleId, required this.icon, }); final String name; final String fontFamily; final String styleId; final IconData icon; } const builtInFontConfigs = [ BuiltInFontConfig(name: '系统默认', fontFamily: 'Inter', styleId: 'system', icon: CupertinoIcons.device_phone_portrait), BuiltInFontConfig(name: '衬线体', fontFamily: 'NotoSerif', styleId: 'serif', icon: CupertinoIcons.book_fill), BuiltInFontConfig(name: '等宽体', fontFamily: 'RobotoMono', styleId: 'mono', icon: CupertinoIcons.keyboard), BuiltInFontConfig(name: '圆体', fontFamily: 'Nunito', styleId: 'rounded', icon: CupertinoIcons.circle_fill), ]; ``` - [ ] **Step 2: Notifier 中使用动态映射替代硬编码 idMap** ```dart String? _getStyleIdForFontFamily(String fontFamily) { final config = builtInFontConfigs.where((c) => c.fontFamily == fontFamily).firstOrNull; return config?.styleId; } ``` 在 `setActiveFont` 中: ```dart final styleId = _getStyleIdForFontFamily(fontFamily); if (styleId != null) { ref.read(themeSettingsProvider.notifier).setCustomFontFamily(''); ref.read(themeSettingsProvider.notifier).setFontStyle(styleId); } ``` - [ ] **Step 3: 运行分析验证并 Commit** ```bash git add -A && git commit -m "refactor: 内置字体映射动态化,消除硬编码idMap" ``` --- ### Task 4: 鸿蒙端字体功能适配 **Files:** - Modify: `lib/features/settings/presentation/font_management_notifier.dart` - [ ] **Step 1: 分析鸿蒙端限制并制定适配策略** 根据项目文档: - `path_provider_ohos` 已适配 ✅ → `getApplicationDocumentsDirectory()` 可用 - `file_picker` 鸿蒙端已适配(版本 8.0.6)✅ → 文件选择可用 - `dio` 纯 Dart ✅ → HTTP 下载可用 - `FontLoader` 是 Flutter 引擎功能 → 鸿蒙端可用 ✅ - 唯一限制:鸿蒙端文件系统路径可能不同,需要验证 - [ ] **Step 2: 移除 isOhos 的 return 限制** 在 `font_management_notifier.dart` 中,移除所有 `if (pu.isOhos) { AppToast.showInfo('鸿蒙端暂不支持...'); return; }` 代码块: - `downloadFont()` — 移除 isOhos 检查 - `importFont()` — 移除 isOhos 检查 - `downloadFontFromUrl()` — 移除 isOhos 检查 保留 `if (pu.isWeb)` 的检查(Web 端确实不支持本地文件操作)。 - [ ] **Step 3: 增加鸿蒙端路径适配** ```dart Future _getFontDirectory() async { if (pu.isWeb) throw UnsupportedError('Web端不支持字体管理'); final dir = await getApplicationDocumentsDirectory(); final fontDir = Directory('${dir.path}/fonts'); if (!await fontDir.exists()) { await fontDir.create(recursive: true); } return fontDir; } ``` 将所有 `getApplicationDocumentsDirectory()` + `fonts` 子目录的操作统一使用 `_getFontDirectory()`。 - [ ] **Step 4: 运行分析验证并 Commit** ```bash git add -A && git commit -m "feat: 鸿蒙端字体功能适配,移除isOhos限制,统一字体目录获取" ``` --- ## Phase 2: 交互体验增强 ### Task 5: 字体预览大图 Sheet **Files:** - Create: `lib/features/settings/presentation/font/font_preview_sheet.dart` - Modify: `lib/features/settings/presentation/font_widgets.dart` - [ ] **Step 1: 创建 FontPreviewSheet 组件** 使用 `AppBottomSheet.showHalf` 实现半屏预览面板: ```dart class FontPreviewSheet extends ConsumerStatefulWidget { const FontPreviewSheet({super.key, required this.font}); final FontInfo font; static Future show(BuildContext context, FontInfo font) { return AppBottomSheet.showHalf( context: context, builder: (_) => FontPreviewSheet(font: font), ); } } class _FontPreviewSheetState extends ConsumerState { double _fontSize = 20.0; final _previewController = TextEditingController(text: '闲言 AaBbCc 你好世界 0123456789'); @override Widget build(BuildContext context) { final ext = AppTheme.ext(context); return Column( children: [ // 标题栏: 字体名 + 关闭按钮 // 自定义预览文本输入框 CupertinoTextField(controller: _previewController, ...) // 字号滑块 CupertinoSlider(value: _fontSize, min: 12, max: 48, onChanged: ...) // 预览区域 Expanded(child: ListView(children: [ // 中文预览 Text(_previewController.text, style: TextStyle(fontFamily: widget.font.fontFamily, fontSize: _fontSize)), // 英文预览 Text('The quick brown fox...', style: TextStyle(fontFamily: widget.font.fontFamily, fontSize: _fontSize * 0.8)), // 数字预览 Text('0123456789', style: TextStyle(fontFamily: widget.font.fontFamily, fontSize: _fontSize * 0.8)), // 字体信息 _buildFontInfo(ext), ])), ], ); } } ``` - [ ] **Step 2: 在 FontItemWidget 和 FontOnlineItemWidget 中添加预览入口** 点击字体项时弹出预览 Sheet(长按或 info 图标): ```dart GestureDetector( onTap: onActivate, onLongPress: () => FontPreviewSheet.show(context, font), child: ... ) ``` - [ ] **Step 3: 运行分析验证并 Commit** ```bash git add -A && git commit -m "feat: 字体预览大图Sheet(半屏面板+自定义预览文本+字号滑块)" ``` --- ### Task 6: 字体卡片滑动删除 **Files:** - Modify: `lib/features/settings/presentation/font_widgets.dart` - [ ] **Step 1: FontItemWidget 外层包裹 AppSlidable** 参考项目中 `note_list_page.dart` 的用法: ```dart AppSlidable( slideKey: ValueKey(font.fontFamily), groupTag: 'font_items', rightActions: [ if (!font.isBuiltIn) SlideActionConfig( type: SlideActionType.delete, onPressed: () async { final confirmed = await AppSlidableDeleteConfirm.show( context, title: '删除 ${font.name}', message: '删除后无法恢复,确定要删除吗?', ); if (confirmed) onDelete?.call(); }, ), SlideActionConfig( type: SlideActionType.favorite, onPressed: () => _toggleFavorite(ref, font), ), ], child: /* 原有 FontItemWidget 内容 */, ) ``` - [ ] **Step 2: 移除原有删除按钮(Icon CupertinoIcons.delete)** 滑动删除替代了原有的删除按钮,非激活态不再显示删除图标。 - [ ] **Step 3: 运行分析验证并 Commit** ```bash git add -A && git commit -m "feat: 字体卡片滑动删除+收藏(AppSlidable),替代原有删除按钮" ``` --- ### Task 7: 搜索/筛选功能 **Files:** - Create: `lib/features/settings/presentation/font/font_search_bar.dart` - Modify: `lib/features/settings/presentation/font_models.dart` - Modify: `lib/features/settings/presentation/font_management_notifier.dart` - Modify: `lib/features/settings/presentation/font_widgets.dart` - [ ] **Step 1: FontManagementState 增加搜索/筛选字段** ```dart class FontManagementState { const FontManagementState({ // ... 现有字段 this.searchQuery = '', this.filterCategory = 'all', }); final String searchQuery; final String filterCategory; List get filteredFonts { var result = fonts; if (searchQuery.isNotEmpty) { final query = searchQuery.toLowerCase(); result = result.where((f) { final pinyinMatch = PinyinHelper.getShortPinyin(f.name).toLowerCase().contains(query); return f.name.toLowerCase().contains(query) || f.fontFamily.toLowerCase().contains(query) || pinyinMatch; }).toList(); } if (filterCategory != 'all') { result = result.where((f) => f.category == filterCategory).toList(); } return result; } } ``` - [ ] **Step 2: Notifier 增加搜索/筛选方法** ```dart void setSearchQuery(String query) { state = state.copyWith(searchQuery: query); } void setFilterCategory(String category) { state = state.copyWith(filterCategory: category); } ``` - [ ] **Step 3: 创建 FontSearchBar 组件** ```dart class FontSearchBar extends ConsumerWidget { const FontSearchBar({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final ext = AppTheme.ext(context); return Row( children: [ Expanded( child: CupertinoSearchTextField( onChanged: (v) => ref.read(fontManagementProvider.notifier).setSearchQuery(v), placeholder: '搜索字体(支持拼音)', style: AppTypography.subhead.copyWith(color: ext.textPrimary), ), ), CupertinoButton( padding: const EdgeInsets.only(left: AppSpacing.xs), onPressed: () => _showFilterSheet(context, ref), child: Icon(CupertinoIcons.slider_horizontal_3, color: ext.accent), ), ], ); } } ``` - [ ] **Step 4: FontLocalSection 使用 filteredFonts 替代 fonts** ```dart final filteredFonts = state.filteredFonts; // ... 使用 filteredFonts 渲染列表 ``` - [ ] **Step 5: 运行分析验证并 Commit** ```bash git add -A && git commit -m "feat: 字体搜索/筛选(支持拼音匹配+分类过滤)" ``` --- ### Task 8: 滚动定位 + 下拉刷新 **Files:** - Modify: `lib/features/settings/presentation/font_management_page.dart` - Modify: `lib/features/settings/presentation/font_widgets.dart` - [ ] **Step 1: FontManagementPage 改为 StatefulWidget + ScrollController** ```dart class FontManagementPage extends ConsumerStatefulWidget { const FontManagementPage({super.key}); @override ConsumerState createState() => _FontManagementPageState(); } class _FontManagementPageState extends ConsumerState { final _scrollController = ScrollController(); final _onlineSectionKey = GlobalKey(); void scrollToOnlineSection() { if (_onlineSectionKey.currentContext != null) { Scrollable.ensureVisible( _onlineSectionKey.currentContext!, duration: const Duration(milliseconds: 500), curve: Curves.easeInOut, ); } } @override void dispose() { _scrollController.dispose(); super.dispose(); } } ``` - [ ] **Step 2: 下拉刷新(CustomRefreshIndicator)** ```dart CustomRefreshIndicator( onRefresh: () => ref.read(fontManagementProvider.notifier).refresh(), builder: (context, child, controller) { return Stack(children: [ child, if (controller.value > 0) Positioned(top: 0, left: 0, right: 0, child: CupertinoActivityIndicator().animate().fadeIn(), ), ]); }, child: ListView(controller: _scrollController, children: [...]), ) ``` - [ ] **Step 3: FontOnlineSection 传入 key** ```dart FontOnlineSection(key: _onlineSectionKey) ``` - [ ] **Step 4: FontQuickActions 的在线字体按钮调用 scrollToOnlineSection** 通过回调传递 `scrollToOnlineSection` 方法给 `FontQuickActions`。 - [ ] **Step 5: 运行分析验证并 Commit** ```bash git add -A && git commit -m "feat: 滚动定位到在线字体区+下拉刷新字体列表" ``` --- ### Task 9: 动画增强(入场动画 + Hero动画 + 骨架屏) **Files:** - Modify: `lib/features/settings/presentation/font_widgets.dart` - Modify: `lib/features/settings/presentation/font_management_page.dart` - [ ] **Step 1: 字体列表项入场动画** 使用项目封装的 `SlideUpItem` 或直接 `flutter_animate`: ```dart FontItemWidget(font: font, ...) .animate() .fadeIn(duration: 300.ms, delay: (index * 50).ms) .slideY(begin: 0.1, end: 0, curve: Curves.easeOutCubic) ``` - [ ] **Step 2: 当前字体卡片 → 预览 Sheet 的 Hero 动画** ```dart Heroine( tag: 'font-preview-${font.fontFamily}', child: Text(font.name, style: ...), ) ``` 在 `FontPreviewSheet` 中对应: ```dart Heroine( tag: 'font-preview-${widget.font.fontFamily}', child: Text(widget.font.name, style: ...), ) ``` - [ ] **Step 3: 字体列表加载骨架屏** 使用项目封装的 `SkeletonBox` + `ListItemSkeleton`: ```dart if (state.isLoading) SkeletonBox(child: Column(children: [ for (var i = 0; i < 4; i++) const ListItemSkeleton(), ])) else ... ``` - [ ] **Step 4: 运行分析验证并 Commit** ```bash git add -A && git commit -m "feat: 字体列表入场动画+Hero过渡+骨架屏加载" ``` --- ### Task 10: 下载增强(并发控制 + 重试 + 进度圆环) **Files:** - Modify: `lib/features/settings/presentation/font_models.dart` - Modify: `lib/features/settings/presentation/font_management_notifier.dart` - Modify: `lib/features/settings/presentation/font_widgets.dart` - [ ] **Step 1: FontManagementState 增加 downloadQueue** ```dart final Set downloadQueue = const {}; ``` - [ ] **Step 2: Notifier 增加并发下载控制** ```dart static const _maxConcurrentDownloads = 3; Future downloadFont(int index) async { if (state.downloadQueue.length >= _maxConcurrentDownloads) { AppToast.showWarning('最多同时下载 $_maxConcurrentDownloads 个字体'); return; } final newQueue = Set.from(state.downloadQueue)..add(index); state = state.copyWith(downloadQueue: newQueue); try { // ... 原有下载逻辑 } finally { final updatedQueue = Set.from(state.downloadQueue)..remove(index); state = state.copyWith(downloadQueue: updatedQueue); } } ``` - [ ] **Step 3: 下载重试机制** ```dart Future retryDownload(int index) async { final updated = List.from(state.onlineFonts); updated[index] = state.onlineFonts[index].copyWith( isDownloading: false, downloadProgress: 0.0, ); state = state.copyWith(onlineFonts: updated); await downloadFont(index); } ``` 在 UI 中,下载失败的字体项显示重试按钮。 - [ ] **Step 4: 下载进度圆环(替代线性进度条)** ```dart SizedBox( width: 28, height: 28, child: Stack(alignment: Alignment.center, children: [ CircularProgressIndicator( value: font.downloadProgress, strokeWidth: 3, backgroundColor: ext.textHint.withValues(alpha: 0.1), valueColor: AlwaysStoppedAnimation(ext.accent), ), Text('${(font.downloadProgress * 100).round()}', style: AppTypography.caption2.copyWith(color: ext.accent, fontSize: 8)), ]), ) ``` - [ ] **Step 5: 运行分析验证并 Commit** ```bash git add -A && git commit -m "feat: 并发下载控制(3个)+重试机制+进度圆环" ``` --- ## Phase 3: 数据动态化与高级功能 ### Task 11: 在线字体数据动态化(Supabase) **Files:** - Create: `lib/features/settings/services/font_sync_service.dart` - Modify: `lib/features/settings/presentation/font_management_notifier.dart` - Modify: `lib/features/settings/presentation/font_models.dart` - [ ] **Step 1: 创建 Supabase fonts 表结构** 在 Supabase 控制台创建 `fonts` 表: ```sql CREATE TABLE fonts ( id UUID DEFAULT gen_random_uuid() PRIMARY KEY, name TEXT NOT NULL, font_family TEXT NOT NULL UNIQUE, download_url TEXT NOT NULL, icon_emoji TEXT DEFAULT '🔤', author TEXT, license TEXT, category TEXT DEFAULT 'sans', tags TEXT[] DEFAULT '{}', language TEXT[] DEFAULT '{"zh","en"}', file_size BIGINT, version TEXT DEFAULT '1.0', download_count INT DEFAULT 0, rating REAL DEFAULT 0, is_active BOOLEAN DEFAULT true, sort_order INT DEFAULT 0, created_at TIMESTAMPTZ DEFAULT now(), updated_at TIMESTAMPTZ DEFAULT now() ); ``` - [ ] **Step 2: 创建 FontSyncService** ```dart class FontSyncService { static SupabaseClient get _client => Supabase.instance.client; static Future> fetchOnlineFonts() async { final response = await _client .from('fonts') .select() .eq('is_active', true) .order('sort_order', ascending: true); return response.map((e) => OnlineFontEntry.fromJson(e)).toList(); } } ``` - [ ] **Step 3: Notifier 中在线字体数据改为从 Supabase 加载** 保留 `onlineFontData` 作为离线 fallback,优先从 Supabase 获取。 - [ ] **Step 4: 运行分析验证并 Commit** ```bash git add -A && git commit -m "feat: 在线字体数据动态化(Supabase远程字体列表+离线fallback)" ``` --- ### Task 12: 字体收藏持久化 **Files:** - Modify: `lib/features/settings/presentation/font_models.dart` - Modify: `lib/features/settings/presentation/font_management_notifier.dart` - Modify: `lib/features/settings/presentation/font_widgets.dart` - [ ] **Step 1: FontInfo 增加 isFavorite 字段** 已在 Task 2 的模型扩展中包含。 - [ ] **Step 2: Notifier 增加收藏方法** ```dart void toggleFavorite(String fontFamily) { final updatedFonts = state.fonts.map((f) { if (f.fontFamily == fontFamily) { return f.copyWith(isFavorite: !f.isFavorite); } return f; }).toList(); state = state.copyWith(fonts: updatedFonts); _saveInstalledFontsToKV(updatedFonts.where((f) => !f.isBuiltIn && f.isDownloaded).toList()); _saveFavoritesToKV(updatedFonts); } void _saveFavoritesToKV(List fonts) { final favIds = fonts.where((f) => f.isFavorite).map((f) => f.fontFamily).toList(); AppKVStore.setStringList('font_favorites', favIds); } ``` - [ ] **Step 3: 收藏字体置顶显示** 在 `filteredFonts` getter 中,收藏的字体排在前面。 - [ ] **Step 4: 运行分析验证并 Commit** ```bash git add -A && git commit -m "feat: 字体收藏持久化(Hive)+收藏置顶显示" ``` --- ### Task 13: 字体包 ZIP 导入 + 批量删除 + 字体分享 **Files:** - Modify: `lib/features/settings/presentation/font_management_notifier.dart` - Modify: `lib/features/settings/presentation/font_widgets.dart` - [ ] **Step 1: ZIP 字体包导入** 使用 `archive` 库: ```dart Future importFontZip() async { if (pu.isWeb) return; try { final result = await FilePicker.platform.pickFiles( type: FileType.custom, allowedExtensions: ['zip'], allowMultiple: false, ); if (result == null || result.files.isEmpty) return; final filePath = result.files.first.path; if (filePath == null) return; final bytes = await File(filePath).readAsBytes(); final archive = ZipDecoder().decodeBytes(bytes); final fontDir = await _getFontDirectory(); int count = 0; for (final file in archive) { if (file.isFile) { final name = file.name.toLowerCase(); if (name.endsWith('.ttf') || name.endsWith('.otf')) { final fileName = file.name.split('/').last; final outputPath = '${fontDir.path}${Platform.pathSeparator}$fileName'; await File(outputPath).writeAsBytes(file.content as List); // 加载字体到引擎... count++; } } } if (count > 0) AppToast.showSuccess('成功导入 $count 个字体 ✅'); } catch (e) { Log.e('ZIP字体导入失败', e); AppToast.showError('导入失败: $e'); } } ``` - [ ] **Step 2: 批量删除** ```dart Future deleteFonts(List fontFamilies) async { for (final family in fontFamilies) { final font = state.fonts.firstWhere((f) => f.fontFamily == family); await deleteFont(family, font.path); } } ``` - [ ] **Step 3: 字体分享** 使用 `share_plus`: ```dart Future shareFont(FontInfo font) async { if (font.sourceUrl != null && font.sourceUrl!.isNotEmpty) { await SharePlus.instance.share(ShareParams(text: '推荐字体: ${font.name}\n下载地址: ${font.sourceUrl}')); } else if (font.path.isNotEmpty) { await SharePlus.instance.share(ShareParams(files: [XFile(font.path)])); } } ``` - [ ] **Step 4: 运行分析验证并 Commit** ```bash git add -A && git commit -m "feat: ZIP字体包导入+批量删除+字体分享" ``` --- ### Task 14: Google Fonts 在线字体库集成 **Files:** - Modify: `lib/features/settings/presentation/font_management_notifier.dart` - Modify: `lib/features/settings/presentation/font_widgets.dart` - [ ] **Step 1: Notifier 增加 Google Fonts 加载方法** 参考项目中 `font_picker.dart` 的 `_safeGoogleFont` 模式: ```dart Future loadGoogleFont(String fontName) async { try { final textStyle = GoogleFonts.getFont(fontName); final fontFamily = textStyle.fontFamily; if (fontFamily == null) return; final loader = FontLoader(fontFamily); // Google Fonts 会自动下载并加载 // 无需手动管理文件 final newFont = FontInfo( name: fontName, fontFamily: fontFamily, path: 'google_fonts://$fontName', isDownloaded: true, category: 'google', sourceUrl: 'https://fonts.google.com/specimen/$fontName', ); final updatedFonts = List.from(state.fonts)..add(newFont); state = state.copyWith(fonts: updatedFonts); AppToast.showSuccess('$fontName 加载成功 ✅'); } catch (e) { Log.e('Google Font 加载失败: $fontName', e); AppToast.showError('字体加载失败'); } } ``` - [ ] **Step 2: 在线字体区增加 Google Fonts 分类入口** - [ ] **Step 3: 运行分析验证并 Commit** ```bash git add -A && git commit -m "feat: Google Fonts在线字体库集成(安全fallback)" ``` --- ### Task 15: 字体对比模式 **Files:** - Create: `lib/features/settings/presentation/font/font_comparison_page.dart` - Modify: `lib/features/settings/presentation/font_widgets.dart` - [ ] **Step 1: 创建 FontComparisonPage** ```dart class FontComparisonPage extends ConsumerStatefulWidget { const FontComparisonPage({super.key, required this.fontA, required this.fontB}); final FontInfo fontA; final FontInfo fontB; } class _FontComparisonPageState extends ConsumerState { final _previewController = TextEditingController(text: '闲言 AaBbCc 你好世界'); double _fontSize = 20.0; @override Widget build(BuildContext context) { return CupertinoPageScaffold( child: SafeArea(child: Row(children: [ Expanded(child: _buildFontPanel(widget.fontA)), Container(width: 1, color: ext.textHint.withValues(alpha: 0.2)), Expanded(child: _buildFontPanel(widget.fontB)), ])), ); } } ``` - [ ] **Step 2: 在字体项长按菜单中增加"对比"选项** - [ ] **Step 3: 运行分析验证并 Commit** ```bash git add -A && git commit -m "feat: 字体对比模式(左右分屏对比两种字体)" ``` --- ## Phase 4: 视觉规范与细节 ### Task 16: Emoji 替换为 Icon/SVG **Files:** - Create: `assets/svgs/font_import.svg`, `assets/svgs/font_url.svg`, `assets/svgs/font_online.svg`, `assets/svgs/font_preview.svg`, `assets/svgs/font_compare.svg`, `assets/svgs/font_favorite.svg`, `assets/svgs/font_zip.svg` - Modify: `lib/shared/widgets/app_icon.dart` - Modify: `lib/features/settings/presentation/font_widgets.dart` - Modify: `lib/features/settings/presentation/font_models.dart` **替换规则:** - 通用功能按钮/标签 → CupertinoIcons(系统自带) - 个性化/品牌化图标 → 本地 SVG - 保留 emoji 的场景:在线字体列表中的字体品牌标识(如 🖋️ 霞鹜文楷、💼 阿里巴巴普惠体) - [ ] **Step 1: 创建 SVG 图标文件** 按 iOS SF Symbols 风格设计,线条粗细 1.5px,24x24 viewBox: - `font_import.svg` — 文件夹+箭头导入 - `font_url.svg` — 链接+下载 - `font_online.svg` — 云+字体 - `font_preview.svg` — 放大镜+文字 - `font_compare.svg` — 双栏对比 - `font_favorite.svg` — 星星+字体 - `font_zip.svg` — ZIP包+字体 - [ ] **Step 2: AppIconData 增加字体相关常量** ```dart class AppIconData { // ... 现有常量 static const String fontImport = 'assets/svgs/font_import.svg'; static const String fontUrl = 'assets/svgs/font_url.svg'; static const String fontOnline = 'assets/svgs/font_online.svg'; static const String fontPreview = 'assets/svgs/font_preview.svg'; static const String fontCompare = 'assets/svgs/font_compare.svg'; static const String fontFavorite = 'assets/svgs/font_favorite.svg'; static const String fontZip = 'assets/svgs/font_zip.svg'; } ``` - [ ] **Step 3: font_widgets.dart 中替换 emoji** | 原始 emoji | 替换为 | 场景 | |-----------|--------|------| | 📁 导入字体 | `AppIcon(svgAsset: AppIconData.fontImport)` | 快捷操作按钮 | | 🔗 URL下载 | `AppIcon(svgAsset: AppIconData.fontUrl)` | 快捷操作按钮 | | ☁️ 在线字体 | `AppIcon(svgAsset: AppIconData.fontOnline)` | 快捷操作按钮+区域标题 | | ✨ 当前字体 | `CupertinoIcons.sparkles` | 区域标题 | | 📱 已安装字体 | `CupertinoIcons.device_phone_portrait` | 区域标题 | | 💡 字体小贴士 | `AppIcon(svgAsset: AppIconData.lightbulb)` | 区域标题 | | 📂 支持.ttf | `CupertinoIcons.doc_text` | 小贴士项 | | 🌐 在线下载 | `CupertinoIcons.cloud_download` | 小贴士项 | | 🔄 切换生效 | `CupertinoIcons.arrow_2_circlepath` | 小贴士项 | | 💾 存储目录 | `CupertinoIcons.folder_fill` | 小贴士项 | | ⚠️ 科学上网 | `CupertinoIcons.exclamationmark_triangle` | 小贴士项 | | 🖋️💼🎉🌸📐📜 | **保留 emoji** | 在线字体品牌标识 | | ⬇️ 下载中 | `CupertinoIcons.arrow_down_circle` | 下载进度 | | 🗑️ 删除 | `CupertinoIcons.trash` | 删除确认 | | 🔤 字体管理 | `CupertinoIcons.textformat_abc` | 导航栏标题 | | 👇 向下滚动 | 移除(改为真正滚动) | Toast 提示 | - [ ] **Step 4: 运行分析验证并 Commit** ```bash git add -A && git commit -m "style: 字体页面emoji替换为CupertinoIcon/SVG,保留品牌emoji" ``` --- ### Task 17: 字体切换音效 **Files:** - Modify: `lib/features/settings/presentation/font_management_notifier.dart` - [ ] **Step 1: 字体切换时播放轻柔反馈音** 使用 `audioplayers`(项目已集成): ```dart static final _audioPlayer = AudioPlayer(); Future _playSwitchSound() async { try { await _audioPlayer.play(AssetSource('sounds/font_switch.mp3')); } catch (_) {} } ``` 在 `setActiveFont` 成功后调用 `_playSwitchSound()`。 - [ ] **Step 2: 运行分析验证并 Commit** ```bash git add -A && git commit -m "feat: 字体切换音效反馈" ``` --- ## Phase 5: 删除后字体残留处理 ### Task 18: 删除字体后的引擎处理 **Files:** - Modify: `lib/features/settings/presentation/font_management_notifier.dart` - [ ] **Step 1: 记录已删除字体列表** Flutter FontLoader 不支持卸载字体,但可以通过记录已删除字体列表,在应用重启时不加载它们: ```dart static const _kvKeyDeletedFonts = 'font_deleted_families'; void _recordDeletedFont(String fontFamily) { final deleted = AppKVStore.getStringList(_kvKeyDeletedFonts) ?? []; if (!deleted.contains(fontFamily)) { deleted.add(fontFamily); AppKVStore.setStringList(_kvKeyDeletedFonts, deleted); } } void _removeFromDeletedList(String fontFamily) { final deleted = AppKVStore.getStringList(_kvKeyDeletedFonts) ?? []; deleted.remove(fontFamily); AppKVStore.setStringList(_kvKeyDeletedFonts, deleted); } ``` - [ ] **Step 2: _init 中过滤已删除字体** 在 `_loadDynamicFonts` 中,跳过已删除的字体: ```dart Future _loadDynamicFonts(List fonts) async { final deletedFamilies = AppKVStore.getStringList(_kvKeyDeletedFonts) ?? []; for (final font in fonts) { if (font.path.isNotEmpty && font.isDownloaded && !deletedFamilies.contains(font.fontFamily)) { await _loadFontIntoEngine(font.fontFamily, font.path); } } } ``` - [ ] **Step 3: 删除时记录 + 重新安装时移除记录** - [ ] **Step 4: 运行分析验证并 Commit** ```bash git add -A && git commit -m "fix: 删除字体后记录已删除列表,重启时不加载已删除字体" ``` --- ## 自审清单 ### 1. Spec 覆盖检查 | 需求 | 对应 Task | |------|-----------| | 1. _applyCustomFont 不完整 | Task 1 ✅ | | 2. 序列化脆弱 | Task 2 ✅ | | 3. 在线字体数据硬编码 | Task 11 ✅ | | 4. 鸿蒙端不可用 | Task 4 ✅ | | 5. 无字体预览大图 | Task 5 ✅ | | 6. 无搜索/筛选 | Task 7 ✅ | | 7. 无并发下载控制 | Task 10 ✅ | | 8. 删除后字体残留 | Task 18 ✅ | | 9. 内置字体映射硬编码 | Task 3 ✅ | | 10. 滚动定位未实现 | Task 8 ✅ | | 11. 无下载重试机制 | Task 10 ✅ | | 12. 无批量操作 | Task 13 ✅ | | 13. 无字体元数据展示 | Task 5 (预览Sheet中) + Task 11 (Supabase) ✅ | | 14. 无自定义预览文本 | Task 5 ✅ | | 字体卡片滑动删除 | Task 6 ✅ | | 字体预览大图 Sheet | Task 5 ✅ | | Google Fonts 集成 | Task 14 ✅ | | 列表项入场动画 | Task 9 ✅ | | 字体切换 Hero 动画 | Task 9 ✅ | | 下拉刷新字体列表 | Task 8 ✅ | | 在线字体数据动态化 | Task 11 ✅ | | 字体收藏持久化 | Task 12 ✅ | | 字体加载骨架屏 | Task 9 ✅ | | 字体包 ZIP 导入 | Task 13 ✅ | | 字体分享 | Task 13 ✅ | | 字体对比模式 | Task 15 ✅ | | 下载进度圆环 | Task 10 ✅ | | 字体搜索拼音 | Task 7 ✅ | | 字体切换音效 | Task 17 ✅ | | emoji→icon替换 | Task 16 ✅ | ### 2. 占位符扫描 无 TBD/TODO/实现后补充等占位符。 ### 3. 类型一致性 所有 Task 中引用的类名、方法名、字段名保持一致: - `FontInfo.isFavorite` / `FontInfo.category` / `FontInfo.author` 等 - `FontManagementState.searchQuery` / `filterCategory` / `downloadQueue` - `ThemeSettingsState.customFontFamily` - `builtInFontConfigs` 替代硬编码 `idMap`