fix: auto repair bugs #80

This commit is contained in:
repair-agent 2026-03-26 11:12:02 +08:00
parent 518c6a26e4
commit ce249058f2
21 changed files with 405 additions and 92 deletions

294
airhub_app/CLAUDE.md Normal file
View File

@ -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<T>` 管理异步状态
3. Repository 返回 `Either<Failure, T>`fpdartController 内 `fold` 处理
4. **页面中优先使用 `ConsumerWidget`**(无状态);需要动画/本地状态时用 `ConsumerStatefulWidget`
5. **`ref.watch()` 用于 UI 绑定**(触发重建),**`ref.read()` 用于事件处理**(不触发重建)
6. **`ref.listen()` 用于副作用**弹窗、导航、Toast不触发 Widget 重建
### 示例
```dart
@riverpod
class MyController extends _$MyController {
@override
FutureOr<MyState> 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<Failure, T>)
→ 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<Duration>? _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` 无 ErrorWarning/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 不会崩溃)

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -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 = <String>{};
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;

View File

@ -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<LoginPage> {
// 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<LoginPage> {
// 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<LoginPage> {
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),

View File

@ -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<BluetoothPage>
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<BluetoothPage>
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<BluetoothPage>
//
Text(
device.name,
style: GoogleFonts.outfit(
style: TextStyle(fontFamily: 'Outfit',
fontSize: 18,
fontWeight: FontWeight.w600,
color: device.isBoundByOther

View File

@ -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<DeviceControlPage>
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<DeviceControlPage>
const SizedBox(width: 4),
Text(
batteryText,
style: GoogleFonts.dmSans(
style: TextStyle(fontFamily: 'DM Sans',
fontSize: 13,
fontWeight: FontWeight.w600,
color: const Color(0xFF4B5563),

View File

@ -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<HomePage>
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<HomePage>
child: Center(
child: Text(
'立即连接',
style: GoogleFonts.dmSans(
style: TextStyle(fontFamily: 'DM Sans',
fontSize: 17,
fontWeight: FontWeight.w600,
color: Colors.white,

View File

@ -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<LoginPage> 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<LoginPage> 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<LoginPage> 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),

View File

@ -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<MusicCreationPage>
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<MusicCreationPage>
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<MusicCreationPage>
),
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<MusicCreationPage>
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<MusicCreationPage>
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<MusicCreationPage>
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<MusicCreationPage>
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

View File

@ -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<ProductSelectionPage> {
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,

View File

@ -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<SettingsPage> {
),
Text(
'设置',
style: GoogleFonts.outfit(
style: TextStyle(fontFamily: 'Outfit',
fontSize: 16,
fontWeight: FontWeight.w600,
color: const Color(0xFF1F2937),

View File

@ -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<WifiConfigPage>
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<WifiConfigPage>
),
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<WifiConfigPage>
),
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<WifiConfigPage>
),
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<WifiConfigPage>
const SizedBox(height: 24),
Text(
'配网成功!',
style: GoogleFonts.outfit(
style: TextStyle(fontFamily: 'Outfit',
fontSize: 24,
fontWeight: FontWeight.bold,
color: const Color(0xFF1F2937),

View File

@ -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',

View File

@ -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,

View File

@ -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),

View File

@ -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,

View File

@ -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:

View File

@ -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