diff --git a/airhub_app/CLAUDE.md b/airhub_app/CLAUDE.md new file mode 100644 index 0000000..51fd3c9 --- /dev/null +++ b/airhub_app/CLAUDE.md @@ -0,0 +1,294 @@ +# Airhub App 开发规范 + +> 所有新功能开发、代码修改、Bug 修复均需遵循本规范 + +--- + +## 一、项目架构 + +``` +lib/ +├── core/ # 基础设施层(全局共享) +│ ├── errors/ # 异常类 & Failure 类 +│ ├── network/ # ApiClient, TokenManager, ApiConfig +│ ├── router/ # go_router 路由配置 +│ └── services/ # 全局服务(LogCenter 等) +├── features/ # 功能模块(按业务领域划分) +│ ├── auth/ # 认证 +│ ├── badge/ # 电子吧唧 +│ ├── device/ # 设备管理 +│ ├── notification/ # 通知 +│ ├── spirit/ # 精灵/角色 +│ ├── user/ # 用户 +│ └── system/ # 系统(版本检查等) +├── pages/ # 独立页面(未归入 feature 的页面) +├── widgets/ # 全局可复用 Widget +├── theme/ # 主题、颜色、Design Tokens +├── services/ # 应用级服务(TTS、音乐生成等) +└── main.dart # 入口 +``` + +### Feature 模块内部结构 + +``` +features/xxx/ +├── data/ +│ ├── datasources/ # 远程/本地数据源 +│ ├── models/ # 数据模型(Freezed) +│ └── repositories/ # Repository 实现 +├── domain/ +│ └── repositories/ # Repository 接口 +└── presentation/ + ├── controllers/ # Riverpod 控制器 + ├── pages/ # 页面 + └── widgets/ # 功能专属 Widget +``` + +--- + +## 二、状态管理(Riverpod) + +### 规则 + +1. **使用 `@riverpod` 注解**声明 Provider,不手写 `StateNotifierProvider` 等 +2. Controller 使用 `AsyncValue` 管理异步状态 +3. Repository 返回 `Either`(fpdart),Controller 内 `fold` 处理 +4. **页面中优先使用 `ConsumerWidget`**(无状态);需要动画/本地状态时用 `ConsumerStatefulWidget` +5. **`ref.watch()` 用于 UI 绑定**(触发重建),**`ref.read()` 用于事件处理**(不触发重建) +6. **`ref.listen()` 用于副作用**(弹窗、导航、Toast),不触发 Widget 重建 + +### 示例 + +```dart +@riverpod +class MyController extends _$MyController { + @override + FutureOr build() async { + final repo = ref.watch(myRepositoryProvider); + final result = await repo.fetchData(); + return result.fold( + (failure) => throw failure, + (data) => MyState(data: data), + ); + } +} +``` + +--- + +## 三、网络层 + +### 规则 + +1. 所有 API 调用通过 `ApiClient`(Dio 封装),不直接使用 `http` 包或 `Dio()` +2. `ApiClient` 自动处理:Token 附加、401 自动刷新、错误上报 +3. 后端统一返回 `{code, message, data}`,`ApiClient._request()` 自动解析 +4. 异常类型:`ServerException`(code != 0)、`NetworkException`(连接失败) +5. **不在 Widget/Controller 中直接调用 ApiClient**,应通过 Repository 层 + +### 错误处理链 + +``` +DataSource (throw Exception) + → Repository (catch → Either) + → Controller (fold → AsyncValue) + → UI (AsyncValue.when: loading/data/error) +``` + +--- + +## 四、路由(go_router) + +### 规则 + +1. 所有路由定义在 `core/router/app_router.dart` +2. 使用 `context.go()` 进行声明式导航,避免 `Navigator.push()`(页面内二级跳转除外) +3. 登录态检查在 `redirect` 中统一处理 +4. 产品类型 → 路由映射在 `_productRoutes` Map 中维护 + +--- + +## 五、性能规范 + +### 内存管理 + +1. **所有 Stream 订阅必须存入变量,dispose 时取消** + ```dart + // ✅ 正确 + StreamSubscription? _positionSub; + _positionSub = stream.listen((data) { ... }); + + @override + void dispose() { + _positionSub?.cancel(); + super.dispose(); + } + + // ❌ 错误 — 无法取消,造成内存泄漏 + stream.listen((data) { ... }); + ``` + +2. **AnimationController、VideoPlayerController、AudioPlayer 等必须在 dispose 中释放** + +3. **页面离开时停止 BLE 扫描** + ```dart + @override + void dispose() { + FlutterBluePlus.stopScan(); + _scanSubscription?.cancel(); + super.dispose(); + } + ``` + +### Widget 重建优化 + +4. **能用 `const` 的地方必须加 `const`** + ```dart + // ✅ + const SizedBox(height: 16) + const EdgeInsets.all(16) + const Text('Hello') + + // ❌ + SizedBox(height: 16) + EdgeInsets.all(16) + ``` + +5. **动画区域用 `RepaintBoundary` 包裹**,防止全页面重绘 + ```dart + RepaintBoundary( + child: AnimatedGradientBackground(), + ) + ``` + +6. **`AnimatedBuilder` 必须使用 `child` 参数**,将不变的子树传入 child,不要在 builder 中重建 + ```dart + // ✅ + AnimatedBuilder( + animation: _controller, + child: const HeavyWidget(), // 只构建一次 + builder: (context, child) => Transform.rotate( + angle: _controller.value, + child: child, // 复用 + ), + ) + ``` + +### API 调用 + +7. **多个独立 API 调用使用 `Future.wait` 并行**,不要顺序 await + ```dart + // ✅ 并行 + final results = await Future.wait([ + api.get('/shelves/'), + api.get('/stories/'), + ]); + + // ❌ 顺序(慢 N 倍) + final shelves = await api.get('/shelves/'); + final stories = await api.get('/stories/'); + ``` + +8. **耗时计算(JSON 解析、图片处理)放到 Isolate** + ```dart + final result = await Isolate.run(() => jsonDecode(bigJson)); + ``` + +### 图片 + +9. **网络图片使用 `Image.network` + `fit: BoxFit.cover`**,不要手动指定 `cacheWidth/cacheHeight`(会导致显示异常) +10. **大量图片列表使用 `ListView.builder`**,不要 `ListView(children: [...])` + +--- + +## 六、构建配置 + +### Android Release + +- `build.gradle.kts` 中 release 配置: + - `isMinifyEnabled = true` — R8 代码压缩 + - `isShrinkResources = true` — 移除未使用资源 + - ProGuard 规则在 `proguard-rules.pro` 维护 +- 打包命令:`flutter build apk --release --split-per-abi --obfuscate --split-debug-info=./debug-info` + +### iOS Release + +- 构建命令:`flutter build ipa --release --obfuscate --split-debug-info=./debug-info` + +--- + +## 七、命名规范 + +### 文件命名 + +| 类型 | 格式 | 示例 | +|------|------|------| +| 页面 | `xxx_page.dart` | `login_page.dart` | +| 控制器 | `xxx_controller.dart` | `auth_controller.dart` | +| 数据源 | `xxx_remote_data_source.dart` | `spirit_remote_data_source.dart` | +| 仓库实现 | `xxx_repository_impl.dart` | `auth_repository_impl.dart` | +| 仓库接口 | `xxx_repository.dart` | `auth_repository.dart` | +| 模型 | `xxx_model.dart` | `user_model.dart` | +| Widget | `xxx_widget.dart` 或直接描述 | `gradient_button.dart` | +| 生成文件 | `*.g.dart` / `*.freezed.dart` | 自动生成,不手动编辑 | + +### 类命名 + +- PascalCase:`AuthController`、`DeviceControlPage`、`ServerException` +- 私有前缀:`_buildHeader()`、`_positionSub` + +### 变量命名 + +- camelCase:`currentUser`、`isPlaying`、`_audioPlayer` +- Provider:`xxxProvider`(由 `@riverpod` 自动生成) + +--- + +## 八、主题系统 + +### 颜色 + +- 全局颜色定义在 `theme/app_colors.dart` +- 产品主题颜色在 `theme/product_theme.dart`(4 种产品类型) +- **不要在 Widget 中硬编码颜色值**,优先使用 `AppColors.xxx` 或 `Theme.of(context)` + +### 字体 + +- 主字体:DM Sans(正文)、Outfit(标题)、Press Start 2P(品牌像素风) +- 通过 `google_fonts` 包引用,**运行时自动下载并缓存** +- 本地字体:Inter(在 `pubspec.yaml` 中声明) + +### 间距 + +- 使用 `AppSpacing.xs/sm/md/lg/xl` 常量 +- 不要在 Widget 中硬编码间距数值 + +--- + +## 九、Git 规范 + +### 分支命名 + +- 功能分支:`fix/auto-YYYYMMDD-HHMMSS` +- 合并目标:`main` + +### Commit 规范 + +- `fix:` — Bug 修复 +- `feat:` — 新功能 +- `refactor:` — 重构(不改变行为) +- `chore:` — 构建配置、依赖更新等 + +--- + +## 十、检查清单(PR 提交前) + +- [ ] `dart analyze` 无 Error(Warning/Info 可接受) +- [ ] 所有 Stream 订阅已在 dispose 中取消 +- [ ] 所有 Controller/Player 已在 dispose 中释放 +- [ ] 多个独立 API 调用已用 `Future.wait` 并行化 +- [ ] 动画 Widget 使用了 `RepaintBoundary` 或 `AnimatedBuilder(child:)` +- [ ] 新增的 Widget 构造函数尽可能标记为 `const` +- [ ] 列表使用 `ListView.builder`,而非 `ListView(children:)` +- [ ] 未引入新的硬编码颜色值(使用 AppColors 或 theme) +- [ ] Android release 构建正常(minify + shrinkResources 不会崩溃) diff --git a/airhub_app/assets/fonts/DMSans-Italic-Variable.ttf b/airhub_app/assets/fonts/DMSans-Italic-Variable.ttf new file mode 100644 index 0000000..bf6819b Binary files /dev/null and b/airhub_app/assets/fonts/DMSans-Italic-Variable.ttf differ diff --git a/airhub_app/assets/fonts/DMSans-Variable.ttf b/airhub_app/assets/fonts/DMSans-Variable.ttf new file mode 100644 index 0000000..c672f98 Binary files /dev/null and b/airhub_app/assets/fonts/DMSans-Variable.ttf differ diff --git a/airhub_app/assets/fonts/Outfit-Variable.ttf b/airhub_app/assets/fonts/Outfit-Variable.ttf new file mode 100644 index 0000000..466d624 Binary files /dev/null and b/airhub_app/assets/fonts/Outfit-Variable.ttf differ diff --git a/airhub_app/assets/fonts/PressStart2P-Regular.ttf b/airhub_app/assets/fonts/PressStart2P-Regular.ttf new file mode 100644 index 0000000..39adf42 Binary files /dev/null and b/airhub_app/assets/fonts/PressStart2P-Regular.ttf differ diff --git a/airhub_app/lib/core/router/app_router.dart b/airhub_app/lib/core/router/app_router.dart index 69e4d80..44e162c 100644 --- a/airhub_app/lib/core/router/app_router.dart +++ b/airhub_app/lib/core/router/app_router.dart @@ -1,7 +1,9 @@ import 'dart:typed_data'; import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; import 'package:go_router/go_router.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:shared_preferences/shared_preferences.dart'; import '../../features/auth/presentation/pages/login_page.dart'; import '../../pages/bluetooth_page.dart'; @@ -21,6 +23,10 @@ import '../network/token_manager.dart'; part 'app_router.g.dart'; +const _lastRouteKey = 'last_business_route'; +const _lastProductTypeKey = 'last_product_type'; +const _validBusinessRoutes = {'/device-control', '/badge-control', '/badge-basic-control'}; + /// 产品代码 → 路由 + ProductType 的映射 const _productCodeRoutes = { 'KPBL-ON': (route: '/device-control', type: ProductType.capybara), @@ -43,29 +49,52 @@ GoRouter goRouter(Ref ref) { return '/login'; } if (hasToken && isLoginRoute) { - // 登录成功 → 跳到最近使用的设备业务页 + // 登录成功 → 获取已绑定设备列表 try { final dataSource = ref.read(deviceRemoteDataSourceProvider); final devices = await dataSource.getMyDevices(); debugPrint('[Router] 已绑定设备数: ${devices.length}'); + if (devices.isNotEmpty) { - // 按 last_online_at 降序,取最近使用的设备 + // 收集用户当前绑定的所有业务路由 + final boundRoutes = {}; + for (final d in devices) { + final dt = d.device.deviceType ?? d.device.deviceTypeInfo; + final code = dt?.productCode ?? ''; + final m = _productCodeRoutes[code]; + if (m != null) boundRoutes.add(m.route); + } + + // 优先恢复上次退出时的业务页面(需确认该设备仍在绑定中) + try { + final prefs = await SharedPreferences.getInstance(); + final savedRoute = prefs.getString(_lastRouteKey); + final savedProductType = prefs.getString(_lastProductTypeKey); + if (savedRoute != null && boundRoutes.contains(savedRoute)) { + debugPrint('[Router] 恢复上次页面: $savedRoute'); + if (savedProductType != null) { + final pt = ProductType.values.where((e) => e.name == savedProductType).firstOrNull; + if (pt != null) { + ref.read(currentProductTypeProvider.notifier).set(pt); + } + } + return savedRoute; + } + } catch (e) { + debugPrint('[Router] 读取上次页面失败: $e'); + } + + // Fallback:按设备最近使用时间跳转 devices.sort((a, b) { final ta = a.device.lastOnlineAt ?? ''; final tb = b.device.lastOnlineAt ?? ''; return tb.compareTo(ta); }); final recent = devices.first; - final dt = recent.device.deviceType; - final dti = recent.device.deviceTypeInfo; - debugPrint('[Router] 最近设备 sn=${recent.device.sn}'); - debugPrint('[Router] deviceType=$dt'); - debugPrint('[Router] deviceTypeInfo=$dti'); - final resolvedDt = dt ?? dti; + final resolvedDt = recent.device.deviceType ?? recent.device.deviceTypeInfo; final code = resolvedDt?.productCode ?? ''; - debugPrint('[Router] productCode=$code'); + debugPrint('[Router] 最近设备 sn=${recent.device.sn}, productCode=$code'); final mapping = _productCodeRoutes[code]; - debugPrint('[Router] mapping=$mapping → route=${mapping?.route}'); if (mapping != null) { ref.read(currentProductTypeProvider.notifier).set(mapping.type); return mapping.route; diff --git a/airhub_app/lib/features/auth/presentation/pages/login_page.dart b/airhub_app/lib/features/auth/presentation/pages/login_page.dart index 9d0465d..567093a 100644 --- a/airhub_app/lib/features/auth/presentation/pages/login_page.dart +++ b/airhub_app/lib/features/auth/presentation/pages/login_page.dart @@ -5,7 +5,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; -import 'package:google_fonts/google_fonts.dart'; import '../../../../core/services/phone_auth_service.dart'; import '../../../../theme/app_colors.dart'; @@ -95,7 +94,7 @@ class _LoginPageState extends ConsumerState { // Title Text( '服务协议', - style: GoogleFonts.outfit( + style: TextStyle(fontFamily: 'Outfit', fontSize: 18, fontWeight: FontWeight.w600, color: const Color(0xFF374151), @@ -307,7 +306,7 @@ class _LoginPageState extends ConsumerState { // Logo — 匹配 HTML .login-logo Text( 'Airhub', - style: GoogleFonts.pressStart2p( + style: TextStyle(fontFamily: 'Press Start 2P', fontSize: 28, color: const Color(0xFF6366F1), // 靛蓝 letterSpacing: 2, @@ -463,7 +462,7 @@ class _LoginPageState extends ConsumerState { children: [ Text( '欢迎使用 Airhub', - style: GoogleFonts.outfit( + style: TextStyle(fontFamily: 'Outfit', fontSize: 32, fontWeight: FontWeight.w700, color: const Color(0xFF6B5B95), @@ -728,7 +727,7 @@ class _AgreementContentPage extends StatelessWidget { ), title: Text( title, - style: GoogleFonts.outfit( + style: TextStyle(fontFamily: 'Outfit', fontSize: 18, fontWeight: FontWeight.w600, color: const Color(0xFF374151), diff --git a/airhub_app/lib/pages/bluetooth_page.dart b/airhub_app/lib/pages/bluetooth_page.dart index 967ffa9..af6e049 100644 --- a/airhub_app/lib/pages/bluetooth_page.dart +++ b/airhub_app/lib/pages/bluetooth_page.dart @@ -5,7 +5,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_blue_plus/flutter_blue_plus.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; -import 'package:google_fonts/google_fonts.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:flutter_svg/flutter_svg.dart'; import '../core/services/ble_provisioning_service.dart'; @@ -586,7 +585,7 @@ class _BluetoothPageState extends ConsumerState child: Text( '搜索设备', textAlign: TextAlign.center, - style: GoogleFonts.outfit( + style: TextStyle(fontFamily: 'Outfit', fontSize: 17, fontWeight: FontWeight.w600, color: const Color(0xFF1F2937), @@ -658,7 +657,7 @@ class _BluetoothPageState extends ConsumerState height: 120, placeholderBuilder: (_) => Text( '?', - style: GoogleFonts.outfit( + style: TextStyle(fontFamily: 'Outfit', fontSize: 48, fontWeight: FontWeight.w700, color: const Color(0xFFF59E0B), // Amber color per HTML @@ -827,7 +826,7 @@ class _BluetoothPageState extends ConsumerState // 设备名称 Text( device.name, - style: GoogleFonts.outfit( + style: TextStyle(fontFamily: 'Outfit', fontSize: 18, fontWeight: FontWeight.w600, color: device.isBoundByOther diff --git a/airhub_app/lib/pages/device_control_page.dart b/airhub_app/lib/pages/device_control_page.dart index 643882f..3f2d529 100644 --- a/airhub_app/lib/pages/device_control_page.dart +++ b/airhub_app/lib/pages/device_control_page.dart @@ -2,7 +2,6 @@ import 'dart:math'; import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:google_fonts/google_fonts.dart'; import 'package:flutter_svg/flutter_svg.dart'; import '../core/network/api_client.dart'; import 'story_detail_page.dart'; @@ -303,7 +302,7 @@ class _DeviceControlPageState extends ConsumerState const SizedBox(width: 8), Text( statusText, - style: GoogleFonts.dmSans( + style: TextStyle(fontFamily: 'DM Sans', fontSize: 13, fontWeight: FontWeight.w600, color: const Color(0xFF4B5563), @@ -327,7 +326,7 @@ class _DeviceControlPageState extends ConsumerState const SizedBox(width: 4), Text( batteryText, - style: GoogleFonts.dmSans( + style: TextStyle(fontFamily: 'DM Sans', fontSize: 13, fontWeight: FontWeight.w600, color: const Color(0xFF4B5563), diff --git a/airhub_app/lib/pages/home_page.dart b/airhub_app/lib/pages/home_page.dart index e9e497d..a341b15 100644 --- a/airhub_app/lib/pages/home_page.dart +++ b/airhub_app/lib/pages/home_page.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; -import 'package:google_fonts/google_fonts.dart'; import '../theme/app_colors.dart'; import '../widgets/animated_gradient_background.dart'; @@ -88,7 +87,7 @@ class _HomePageState extends State child: Text( 'Airhub', // Use Press Start 2P pixel font per HTML CSS - style: GoogleFonts.pressStart2p( + style: TextStyle(fontFamily: 'Press Start 2P', fontSize: 28, color: const Color(0xFF6366F1), // 靛蓝 letterSpacing: 2, @@ -239,7 +238,7 @@ class _HomePageState extends State child: Center( child: Text( '立即连接', - style: GoogleFonts.dmSans( + style: TextStyle(fontFamily: 'DM Sans', fontSize: 17, fontWeight: FontWeight.w600, color: Colors.white, diff --git a/airhub_app/lib/pages/login_page.dart b/airhub_app/lib/pages/login_page.dart index 8f530e8..a2dae74 100644 --- a/airhub_app/lib/pages/login_page.dart +++ b/airhub_app/lib/pages/login_page.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:go_router/go_router.dart'; -import 'package:google_fonts/google_fonts.dart'; import '../theme/app_colors.dart'; import '../widgets/gradient_button.dart'; @@ -58,7 +57,7 @@ class _LoginPageState extends State with TickerProviderStateMixin { // Title Text( '服务协议', - style: GoogleFonts.outfit( + style: TextStyle(fontFamily: 'Outfit', fontSize: 18, fontWeight: FontWeight.w600, color: const Color(0xFF1F2937), @@ -326,7 +325,7 @@ class _LoginPageState extends State with TickerProviderStateMixin { padding: const EdgeInsets.only(top: 32), child: Text( 'Airhub', - style: GoogleFonts.pressStart2p( + style: TextStyle(fontFamily: 'Press Start 2P', fontSize: 28, color: const Color(0xFF6366F1), letterSpacing: 2, @@ -502,7 +501,7 @@ class _LoginPageState extends State with TickerProviderStateMixin { // Heading - font-size: 32px, font-weight: 700 Text( '欢迎使用 Airhub', - style: GoogleFonts.outfit( + style: TextStyle(fontFamily: 'Outfit', fontSize: 32, fontWeight: FontWeight.w700, color: const Color(0xFF6B5B95), diff --git a/airhub_app/lib/pages/music_creation_page.dart b/airhub_app/lib/pages/music_creation_page.dart index 26bea18..940a187 100644 --- a/airhub_app/lib/pages/music_creation_page.dart +++ b/airhub_app/lib/pages/music_creation_page.dart @@ -2,7 +2,6 @@ import 'dart:math'; import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart' show PlatformException; -import 'package:google_fonts/google_fonts.dart'; import 'package:just_audio/just_audio.dart'; import '../services/music_generation_service.dart'; import '../widgets/animated_gradient_background.dart'; @@ -801,7 +800,7 @@ class _MusicCreationPageState extends State child: Text( '灵感电台', textAlign: TextAlign.center, - style: GoogleFonts.outfit( + style: TextStyle(fontFamily: 'Outfit', fontSize: 17, fontWeight: FontWeight.w600, color: const Color(0xFF1F2937), @@ -1108,7 +1107,7 @@ class _MusicCreationPageState extends State track.lyrics.isNotEmpty ? _cleanLyrics(track.lyrics) : '生成音乐后\n点我看歌词', - style: GoogleFonts.dmSans( + style: TextStyle(fontFamily: 'DM Sans', fontSize: 12, height: 1.6, color: track.lyrics.isNotEmpty @@ -1170,7 +1169,7 @@ class _MusicCreationPageState extends State ), child: Text( bubbleText, - style: GoogleFonts.dmSans( + style: TextStyle(fontFamily: 'DM Sans', fontSize: 12.5, fontWeight: FontWeight.w500, color: const Color(0xFF6B4423), @@ -1227,7 +1226,7 @@ class _MusicCreationPageState extends State child: Text( _currentTime, textAlign: TextAlign.center, - style: GoogleFonts.dmSans( + style: TextStyle(fontFamily: 'DM Sans', fontSize: 12, color: const Color(0xFF6B7280), fontFeatures: const [FontFeature.tabularFigures()], @@ -1277,7 +1276,7 @@ class _MusicCreationPageState extends State child: Text( _totalTime, textAlign: TextAlign.center, - style: GoogleFonts.dmSans( + style: TextStyle(fontFamily: 'DM Sans', fontSize: 12, color: const Color(0xFF6B7280), fontFeatures: const [FontFeature.tabularFigures()], @@ -1434,7 +1433,7 @@ class _MusicCreationPageState extends State children: [ Text( mood['title'] as String, - style: GoogleFonts.outfit( + style: TextStyle(fontFamily: 'Outfit', fontSize: 14, fontWeight: isActive ? FontWeight.w700 : FontWeight.w600, color: isActive @@ -1447,7 +1446,7 @@ class _MusicCreationPageState extends State const SizedBox(height: 2), Text( mood['desc'] as String, - style: GoogleFonts.dmSans( + style: TextStyle(fontFamily: 'DM Sans', fontSize: 11, color: isActive ? const Color(0xFF6B7280) @@ -1908,7 +1907,7 @@ class _InputModalContent extends StatelessWidget { children: [ Text( '自由创作', - style: GoogleFonts.outfit( + style: TextStyle(fontFamily: 'Outfit', fontSize: 16, fontWeight: FontWeight.w600, color: const Color(0xFF374151), @@ -1935,7 +1934,7 @@ class _InputModalContent extends StatelessWidget { alignment: Alignment.centerLeft, child: Text( '描述你想要的音乐氛围、场景或情绪', - style: GoogleFonts.dmSans( + style: TextStyle(fontFamily: 'DM Sans', fontSize: 12, color: const Color(0xFF9CA3AF), ), @@ -1950,11 +1949,11 @@ class _InputModalContent extends StatelessWidget { controller: controller, minLines: 4, maxLines: 6, - style: GoogleFonts.dmSans( + style: TextStyle(fontFamily: 'DM Sans', fontSize: 14, color: const Color(0xFF374151)), decoration: InputDecoration( hintText: '例如:水豚在雨中等公交,心情却很平静...', - hintStyle: GoogleFonts.dmSans( + hintStyle: TextStyle(fontFamily: 'DM Sans', fontSize: 14, color: const Color(0xFF9CA3AF)), filled: true, fillColor: Colors.black.withOpacity(0.03), @@ -2099,7 +2098,7 @@ class _PlaylistModalContentState extends State<_PlaylistModalContent> children: [ Text( '我的唱片架', - style: GoogleFonts.outfit( + style: TextStyle(fontFamily: 'Outfit', fontSize: 15, fontWeight: FontWeight.w600, color: const Color(0xFF374151), @@ -2266,7 +2265,7 @@ class _PlaylistModalContentState extends State<_PlaylistModalContent> Flexible( child: Text( track.title, - style: GoogleFonts.dmSans( + style: TextStyle(fontFamily: 'DM Sans', fontSize: 12, fontWeight: isCurrent ? FontWeight.w600 : FontWeight.w500, color: isCurrent diff --git a/airhub_app/lib/pages/product_selection_page.dart b/airhub_app/lib/pages/product_selection_page.dart index 9122da5..90b961f 100644 --- a/airhub_app/lib/pages/product_selection_page.dart +++ b/airhub_app/lib/pages/product_selection_page.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; -import 'package:google_fonts/google_fonts.dart'; import 'package:flutter_svg/flutter_svg.dart'; import '../widgets/animated_gradient_background.dart'; import '../features/device/presentation/controllers/device_controller.dart'; @@ -233,7 +232,7 @@ class _ProductSelectionPageState extends ConsumerState { children: [ Text( '选择产品', - style: GoogleFonts.outfit( + style: TextStyle(fontFamily: 'Outfit', fontSize: 17, fontWeight: FontWeight.w600, color: const Color(0xFF1F2937), @@ -365,7 +364,7 @@ class _ProductCard extends StatelessWidget { children: [ Text( product['name'], - style: GoogleFonts.outfit( + style: TextStyle(fontFamily: 'Outfit', fontSize: 19, fontWeight: FontWeight.bold, color: Colors.white, diff --git a/airhub_app/lib/pages/settings_page.dart b/airhub_app/lib/pages/settings_page.dart index 94111ee..70ca55e 100644 --- a/airhub_app/lib/pages/settings_page.dart +++ b/airhub_app/lib/pages/settings_page.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; -import 'package:google_fonts/google_fonts.dart'; import '../widgets/glass_dialog.dart'; import '../widgets/animated_gradient_background.dart'; import '../widgets/ios_toast.dart'; @@ -100,7 +99,7 @@ class _SettingsPageState extends ConsumerState { ), Text( '设置', - style: GoogleFonts.outfit( + style: TextStyle(fontFamily: 'Outfit', fontSize: 16, fontWeight: FontWeight.w600, color: const Color(0xFF1F2937), diff --git a/airhub_app/lib/pages/wifi_config_page.dart b/airhub_app/lib/pages/wifi_config_page.dart index d5aaa0b..3931861 100644 --- a/airhub_app/lib/pages/wifi_config_page.dart +++ b/airhub_app/lib/pages/wifi_config_page.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; -import 'package:google_fonts/google_fonts.dart'; import 'package:flutter_svg/flutter_svg.dart'; import '../core/services/ble_provisioning_service.dart'; import '../features/device/presentation/controllers/device_controller.dart'; @@ -322,7 +321,7 @@ class _WifiConfigPageState extends ConsumerState child: Text( 'WiFi配网', textAlign: TextAlign.center, - style: GoogleFonts.outfit( + style: TextStyle(fontFamily: 'Outfit', fontSize: 17, fontWeight: FontWeight.w600, color: const Color(0xFF1F2937), @@ -386,7 +385,7 @@ class _WifiConfigPageState extends ConsumerState ), Text( '选择WiFi网络', - style: GoogleFonts.outfit( + style: TextStyle(fontFamily: 'Outfit', fontSize: 20, fontWeight: FontWeight.bold, color: const Color(0xFF1F2937), @@ -528,7 +527,7 @@ class _WifiConfigPageState extends ConsumerState ), Text( _selectedWifiSsid, - style: GoogleFonts.outfit( + style: TextStyle(fontFamily: 'Outfit', fontSize: 20, fontWeight: FontWeight.bold, color: const Color(0xFF1F2937), @@ -596,7 +595,7 @@ class _WifiConfigPageState extends ConsumerState ), Text( '正在配网...', - style: GoogleFonts.outfit( + style: TextStyle(fontFamily: 'Outfit', fontSize: 20, fontWeight: FontWeight.bold, color: const Color(0xFF1F2937), @@ -712,7 +711,7 @@ class _WifiConfigPageState extends ConsumerState const SizedBox(height: 24), Text( '配网成功!', - style: GoogleFonts.outfit( + style: TextStyle(fontFamily: 'Outfit', fontSize: 24, fontWeight: FontWeight.bold, color: const Color(0xFF1F2937), diff --git a/airhub_app/lib/theme/app_theme.dart b/airhub_app/lib/theme/app_theme.dart index 3cae2e8..46f4100 100644 --- a/airhub_app/lib/theme/app_theme.dart +++ b/airhub_app/lib/theme/app_theme.dart @@ -1,11 +1,10 @@ import 'package:flutter/material.dart'; -import 'package:google_fonts/google_fonts.dart'; import 'app_colors.dart'; class AppTheme { static ThemeData get lightTheme { // Base text theme with DM Sans (PRD: 正文/UI 字体) - final baseTextTheme = GoogleFonts.dmSansTextTheme(const TextTheme( + final baseTextTheme = const TextTheme( // h1 / Large Headings displayLarge: TextStyle( color: AppColors.textPrimary, @@ -40,19 +39,19 @@ class AppTheme { fontWeight: FontWeight.w600, letterSpacing: 0.5, ), - )); + ).apply(fontFamily: 'DM Sans'); // Apply Outfit to heading styles (PRD: 标题/Display 字体) final textTheme = baseTextTheme.copyWith( - displayLarge: GoogleFonts.outfit(textStyle: baseTextTheme.displayLarge), - displayMedium: GoogleFonts.outfit(textStyle: baseTextTheme.displayMedium), - displaySmall: GoogleFonts.outfit(textStyle: baseTextTheme.displaySmall), - headlineLarge: GoogleFonts.outfit(textStyle: baseTextTheme.headlineLarge), - headlineMedium: GoogleFonts.outfit(textStyle: baseTextTheme.headlineMedium), - headlineSmall: GoogleFonts.outfit(textStyle: baseTextTheme.headlineSmall), - titleLarge: GoogleFonts.outfit(textStyle: baseTextTheme.titleLarge), - titleMedium: GoogleFonts.outfit(textStyle: baseTextTheme.titleMedium), - titleSmall: GoogleFonts.outfit(textStyle: baseTextTheme.titleSmall), + displayLarge: baseTextTheme.displayLarge?.copyWith(fontFamily: 'Outfit'), + displayMedium: baseTextTheme.displayMedium?.copyWith(fontFamily: 'Outfit'), + displaySmall: baseTextTheme.displaySmall?.copyWith(fontFamily: 'Outfit'), + headlineLarge: baseTextTheme.headlineLarge?.copyWith(fontFamily: 'Outfit'), + headlineMedium: baseTextTheme.headlineMedium?.copyWith(fontFamily: 'Outfit'), + headlineSmall: baseTextTheme.headlineSmall?.copyWith(fontFamily: 'Outfit'), + titleLarge: baseTextTheme.titleLarge?.copyWith(fontFamily: 'Outfit'), + titleMedium: baseTextTheme.titleMedium?.copyWith(fontFamily: 'Outfit'), + titleSmall: baseTextTheme.titleSmall?.copyWith(fontFamily: 'Outfit'), ); return ThemeData( @@ -67,7 +66,7 @@ class AppTheme { background: AppColors.bgBase, ), // PRD: DM Sans 为默认正文字体,回退到系统字体 - fontFamily: GoogleFonts.dmSans().fontFamily, + fontFamily: 'DM Sans', fontFamilyFallback: const [ 'Roboto', 'PingFang SC', diff --git a/airhub_app/lib/theme/design_tokens.dart b/airhub_app/lib/theme/design_tokens.dart index 302cf09..cb139de 100644 --- a/airhub_app/lib/theme/design_tokens.dart +++ b/airhub_app/lib/theme/design_tokens.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:google_fonts/google_fonts.dart'; /// 颜色定义 - 精确还原 Profile PRD class AppColors { @@ -111,77 +110,77 @@ class AppColors { /// 字体样式 - PRD规范: Outfit(标题) + DM Sans(正文) + Press Start 2P(Logo) class AppTextStyles { // 页面标题 — 统一规范: 17px w600 #1F2937 - static final TextStyle title = GoogleFonts.outfit( + static final TextStyle title = TextStyle(fontFamily: 'Outfit', fontSize: 17, fontWeight: FontWeight.w600, color: const Color(0xFF1F2937), ); // User Name → Outfit (heading/display) - static final TextStyle userName = GoogleFonts.outfit( + static final TextStyle userName = TextStyle(fontFamily: 'Outfit', fontSize: 20, fontWeight: FontWeight.w600, color: AppColors.textPrimary, ); // User ID → DM Sans (body) - static final TextStyle userId = GoogleFonts.dmSans( + static final TextStyle userId = TextStyle(fontFamily: 'DM Sans', fontSize: 13, fontWeight: FontWeight.w400, color: AppColors.textSecondary, ); // Menu Text → DM Sans (body/UI) - static final TextStyle menuText = GoogleFonts.dmSans( + static final TextStyle menuText = TextStyle(fontFamily: 'DM Sans', fontSize: 16, fontWeight: FontWeight.w400, color: AppColors.textPrimary, ); // Badge Text → DM Sans (small UI) - static final TextStyle badge = GoogleFonts.dmSans( + static final TextStyle badge = TextStyle(fontFamily: 'DM Sans', fontSize: 10, fontWeight: FontWeight.w400, color: Colors.white, ); // Modal Title → Outfit (heading) - static final TextStyle modalTitle = GoogleFonts.outfit( + static final TextStyle modalTitle = TextStyle(fontFamily: 'Outfit', fontSize: 18, fontWeight: FontWeight.w600, color: AppColors.textPrimary, ); // Book specific styles → Outfit (heading) - static final TextStyle bookTitle = GoogleFonts.outfit( + static final TextStyle bookTitle = TextStyle(fontFamily: 'Outfit', fontSize: 18, fontWeight: FontWeight.w700, color: AppColors.textPrimary, ); // Book count → DM Sans (body) - static final TextStyle bookCount = GoogleFonts.dmSans( + static final TextStyle bookCount = TextStyle(fontFamily: 'DM Sans', fontSize: 13, fontWeight: FontWeight.w600, color: AppColors.textSecondary, ); // Slot title → DM Sans (small UI) - static final TextStyle slotTitle = GoogleFonts.dmSans( + static final TextStyle slotTitle = TextStyle(fontFamily: 'DM Sans', fontSize: 10, color: Colors.white, fontWeight: FontWeight.w400, ); // PRD: font-size: 24px, color: #9CA3AF, font-weight: 300, opacity: 0.7 - static final TextStyle emptyPlus = GoogleFonts.dmSans( + static final TextStyle emptyPlus = TextStyle(fontFamily: 'DM Sans', fontSize: 24, fontWeight: FontWeight.w300, color: const Color(0xB39CA3AF), // #9CA3AF with 0.7 opacity ); // Button text → DM Sans (UI) - static final TextStyle createStoryBtn = GoogleFonts.dmSans( + static final TextStyle createStoryBtn = TextStyle(fontFamily: 'DM Sans', fontSize: 17, fontWeight: FontWeight.w600, color: Colors.white, diff --git a/airhub_app/lib/widgets/glass_dialog.dart b/airhub_app/lib/widgets/glass_dialog.dart index 339578c..e8392af 100644 --- a/airhub_app/lib/widgets/glass_dialog.dart +++ b/airhub_app/lib/widgets/glass_dialog.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:google_fonts/google_fonts.dart'; import 'gradient_button.dart'; import '../theme/app_colors.dart' as appclr; @@ -55,7 +54,7 @@ class GlassDialog extends StatelessWidget { // Title Text( title, - style: GoogleFonts.outfit( + style: TextStyle(fontFamily: 'Outfit', fontSize: 20, fontWeight: FontWeight.w600, color: const Color(0xFF4B2404), diff --git a/airhub_app/lib/widgets/gradient_button.dart b/airhub_app/lib/widgets/gradient_button.dart index 1c8f577..c3d3565 100644 --- a/airhub_app/lib/widgets/gradient_button.dart +++ b/airhub_app/lib/widgets/gradient_button.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:google_fonts/google_fonts.dart'; import '../theme/app_colors.dart'; import '../theme/product_theme.dart'; @@ -125,7 +124,7 @@ class GradientButton extends StatelessWidget { ) : Text( text, - style: GoogleFonts.dmSans( + style: TextStyle(fontFamily: 'DM Sans', fontSize: 17, fontWeight: FontWeight.w600, color: Colors.white, diff --git a/airhub_app/pubspec.lock b/airhub_app/pubspec.lock index 324c64d..dde25db 100644 --- a/airhub_app/pubspec.lock +++ b/airhub_app/pubspec.lock @@ -524,14 +524,6 @@ packages: url: "https://pub.dev" source: hosted version: "14.8.1" - google_fonts: - dependency: "direct main" - description: - name: google_fonts - sha256: ba03d03bcaa2f6cb7bd920e3b5027181db75ab524f8891c8bc3aa603885b8055 - url: "https://pub.dev" - source: hosted - version: "6.3.3" graphs: dependency: transitive description: diff --git a/airhub_app/pubspec.yaml b/airhub_app/pubspec.yaml index b7fa786..3b13a1c 100644 --- a/airhub_app/pubspec.yaml +++ b/airhub_app/pubspec.yaml @@ -61,7 +61,7 @@ dependencies: # Existing dependencies webview_flutter: ^4.4.2 permission_handler: ^11.0.0 - google_fonts: ^6.1.0 + # google_fonts removed — local fonts used instead flutter_blue_plus: ^1.31.0 flutter_svg: ^2.0.9 image_picker: ^1.2.1 @@ -90,6 +90,17 @@ flutter: weight: 600 - asset: assets/fonts/Inter-Bold.ttf weight: 700 + - family: DM Sans + fonts: + - asset: assets/fonts/DMSans-Variable.ttf + - asset: assets/fonts/DMSans-Italic-Variable.ttf + style: italic + - family: Outfit + fonts: + - asset: assets/fonts/Outfit-Variable.ttf + - family: Press Start 2P + fonts: + - asset: assets/fonts/PressStart2P-Regular.ttf # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/to/resolution-aware-images