29 Commits

Author SHA1 Message Date
lxh2875931338 c85a523ec0 机型匹配情况 2026-07-21 19:21:05 +08:00
lxh2875931338 067f0d17e8 机型匹配情况说明 2026-07-21 19:19:50 +08:00
lxh2875931338 a63be9f675 20inf最低支持版本说明 2026-07-21 19:15:43 +08:00
lxh2875931338 86c38a97f5 增加机型匹配信息与适配说明 2026-07-21 19:13:27 +08:00
lxh2875931338 2026e55fde readme机型匹配列表更新 2026-07-21 18:57:35 +08:00
lxh2875931338 286162d41b readme机型匹配列表更新 2026-07-21 18:56:49 +08:00
lxh2875931338 4006684466 Merge branch 'main' of http://192.168.3.16:3800/lxh2875931338/MEIZU-Battery-Healthy 2026-07-21 18:54:50 +08:00
lxh2875931338 e5856296d4 readme机型匹配列表更新 2026-07-21 18:54:19 +08:00
lxh2875931338 e049cab9ad readme机型匹配列表更新 2026-07-21 18:49:27 +08:00
lxh2875931338 2b3d9471cd 补充版本号更新:1.6.0-Release 2026-07-21 18:39:26 +08:00
lxh2875931338 76fd0417e7 紧急补充:魅族21note字段更新 2026-07-21 00:09:47 +08:00
lxh2875931338 471d8310d7 更新日志 2026-07-20 23:59:33 +08:00
lxh2875931338 92f6c30699 更新日志说明 2026-07-20 23:58:48 +08:00
lxh2875931338 2e438aa089 针对readme部分做性能优化,目标帧率120达成 2026-07-20 23:57:01 +08:00
lxh2875931338 5e755d860f 更新日志界面优化、主页部分动画调整 2026-07-20 23:34:42 +08:00
lxh2875931338 0d8c6b081d 更新日志铺路部分,dev中 2026-07-20 23:16:39 +08:00
lxh2875931338 c8866a740a 21note字段确认为乱码,撤回支持 2026-07-20 22:49:16 +08:00
lxh2875931338 41f3ea114b 21Note字段更新(字段预测) 2026-07-20 22:39:17 +08:00
lxh2875931338 b5628510f6 readme更新 2026-07-19 20:57:04 +08:00
lxh2875931338 b390311827 版本号调整 2026-07-19 20:14:32 +08:00
lxh2875931338 f3a414b097 readme更新 2026-07-19 20:05:47 +08:00
lxh2875931338 cf7cec6be3 readme更新 2026-07-19 19:55:32 +08:00
lxh2875931338 6808f099f4 去除魅族22的机型匹配信息(机型没有响应的log文件) 2026-07-19 19:50:20 +08:00
lxh2875931338 38624a8d50 修改机型名称(匹配其他机型的非全大写) 2026-07-19 19:48:55 +08:00
lxh2875931338 b34af6af66 20inf识别字更新 2026-07-19 19:48:07 +08:00
lxh2875931338 ae9bb2dfa1 1.5.2版本发布及部分readme更新 2026-07-14 23:03:35 +08:00
lxh2875931338 bb9a3dc3f5 浪漫雅圆(FlymeFont.ttf)脱色处理并添加代码适配 2026-07-14 22:58:37 +08:00
lxh2875931338 adc49fa1a5 更新一个字体 2026-07-14 21:51:44 +08:00
lxh2875931338 804b992ee4 readme修改,涉及机型匹配相关信息调整 2026-07-11 21:41:59 +08:00
18 changed files with 1021 additions and 291 deletions
-3
View File
@@ -1,3 +0,0 @@
{
"cmake.ignoreCMakeListsMissing": true
}
+40 -4
View File
@@ -25,15 +25,51 @@
| 版本 | 主题 | 状态 | | 版本 | 主题 | 状态 |
| :--- | :--- | :--- | | :--- | :--- | :--- |
| 1.5.0 | 功率计算优化 | ✅ 已发布 | | 1.5.0 | 功率计算优化 | ✅ 已发布 |
| 2.0.0 | 数据看板扩展(月/季/年趋势) | 📋 远景 | | 1.5.2 | 部分场景字体替换 | ✅ 已发布 |
| 1.6.0 | 更新日志独立页面 | ✅ 已发布 |
| 1.7.0 | 适配夜间模式 | 📋近期开发 |
| 2.0.0 | 数据看板月份显示扩展等系列更新 | 📋 远景 |
| 3.0.0 | 搭配PC上位机记录充电信息等 | 📋 远景 |
--- ---
## 环境要求 ## 环境要求
- Flutter SDK: ^3.9.2 - Flutter SDK: ^3.9.2
- Android: 8.0+ (API 26+) - Android: 8.0+ (API 26+)
- 仅支持魅族手机(15 系列 ~ 22 系列)
---
## 机型匹配情况
| 机型 | 系统版本 | 容量来源 | 设计容量 | 状态 |
|---|---|---|---|---|
| 魅族 22 | Flyme 12.x | 系统未生成 `mbattery_charger` 目录 | 5510 mAh | ❌ 不支持 |
| 魅族 21 | Flyme 12.x | `learned_cap` | 4800 mAh | ✅ 与 21 Pro 同步获得支持 |
| 魅族 21 Pro | Flyme 12.x | `learned_cap` | 5050 mAh | ✅ 初始支持 |
| 魅族 21 Note | Flyme 12.x | `learned_cap` | 5500 mAh | ✅ 1.6.0 新增支持 |
| 魅族 21 Note | Flyme 10 / 11 | 日志格式异常 | — | ❌ 不支持 |
| 魅族 20 | Flyme 12.x | `remaining_cap` + `status=Full` | 4700 mAh | ✅ 1.4.0 首次支持 |
| 魅族 20 Pro | Flyme 12.x | `remaining_cap` + `status=Full` | 5000 mAh | ✅ 1.4.0 首次支持 |
| 魅族 20 INFINITY | Flyme 10.x | `remaining_cap` + `status=Full` | 4800 mAh | ✅ 1.5.3 字段匹配 |
| 魅族 18 / 18s / 18 Pro / 18s Pro / 18X | Flyme 9 | 日志停止生成 | 4000 / 4500 mAh | ❌ 无适配条件 |
| 魅族 18 / 18s / 18 Pro / 18s Pro / 18X | Flyme 10 | 不再主动适配 | 4000 / 4500 mAh | ⚠️ 实验性支持 |
| 魅族 17 / 17Pro | Flyme 8 / 9 / 10 | 等待群友提供字段信息 | 4500 / 4500mAh | ⚠️ 待验证 |
| 魅族 16th / 16th Plus / 16X / 16s / 16sPro / 16Xs / 16T | Flyme 7 / 8 / 9 | 需回退原厂固件验证 | - | ⚠️ 若新版本无日志,则变为实验性支持,不主动适配 |
### 状态说明
| 状态 | 含义 |
|---|---|
| ✅ 完美支持 | 已通过实机验证,所有字段完整解析 |
| ⚠️ 实验性支持 | 理论上可支持,但需要用户协助验证 |
| ⚠️ 待验证 | 尚未收到实机日志反馈,待确认 |
| ❌ 不支持 | 系统未生成 BMS 日志,或日志格式无法解析 |
### 特别说明
- **Flyme 版本差异**:同一机型在不同 Flyme 版本下日志格式可能不同。例如 21 Note 在 Flyme 12.x 下可正常读取 `learned_cap`,但在 Flyme 10/11 下日志格式异常。
- **日志停止生成**:部分机型在升级到新系统后(如 17/18 系升级到 Flyme 10),系统不再向 `mbattery_charger` 目录写入新日志,文件夹虽存在但停止更新。
- **适配策略**:优先支持日志格式稳定的 Flyme 12.x 机型,旧版本系统暂不主动适配,若有用户长期停留在旧版本并愿意协助测试,可酌情增加支持。
--- ---
@@ -44,7 +80,7 @@
flutter pub get flutter pub get
# Debug 版本 # Debug 版本
flutter run flutter run --debug
# Release APK # Release APK
flutter build apk --release --target-platform android-arm64 flutter build apk --release --target-platform android-arm64
@@ -11,6 +11,7 @@ import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.provider.DocumentsContract import android.provider.DocumentsContract
import android.provider.OpenableColumns import android.provider.OpenableColumns
import android.view.Window // 🟢 必须包含
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.annotation.NonNull import androidx.annotation.NonNull
import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.android.FlutterActivity
@@ -32,6 +33,9 @@ class MainActivity : FlutterActivity() {
private val PREF_NAME = "battery_health_prefs" private val PREF_NAME = "battery_health_prefs"
private val KEY_SAF_URI = "saf_uri" private val KEY_SAF_URI = "saf_uri"
// 🟢 刷新率控制通道
private val FRAME_RATE_CHANNEL = "com.lxh.battery_health/frame_rate"
private val chargeReceiver = object : BroadcastReceiver() { private val chargeReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) { override fun onReceive(context: Context?, intent: Intent?) {
intent ?: return intent ?: return
@@ -74,7 +78,6 @@ class MainActivity : FlutterActivity() {
try { try {
safUri = Uri.parse(uriString) safUri = Uri.parse(uriString)
android.util.Log.d("BatteryHealth", "通过 MethodChannel 恢复 URI: $safUri") android.util.Log.d("BatteryHealth", "通过 MethodChannel 恢复 URI: $safUri")
// 同时保存到 SharedPreferences
val prefs: SharedPreferences = getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE) val prefs: SharedPreferences = getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)
prefs.edit().putString(KEY_SAF_URI, uriString).apply() prefs.edit().putString(KEY_SAF_URI, uriString).apply()
} catch (e: Exception) { } catch (e: Exception) {
@@ -222,9 +225,34 @@ class MainActivity : FlutterActivity() {
return fileUri?.let { readFile(it) } return fileUri?.let { readFile(it) }
} }
// 🟢 刷新率控制方法
// 🟢 刷新率控制方法(使用 preferredRefreshRate,兼容性更好)
private fun setHighRefreshRate() {
try {
val params = window.attributes
params.preferredRefreshRate = 120f
window.attributes = params
android.util.Log.d("BatteryHealth", "setHighRefreshRate: 设置 120Hz")
} catch (e: Exception) {
android.util.Log.e("BatteryHealth", "setHighRefreshRate 失败: ${e.message}")
}
}
private fun clearRefreshRate() {
try {
val params = window.attributes
params.preferredRefreshRate = 0f // 0 表示让系统自动决定
window.attributes = params
android.util.Log.d("BatteryHealth", "clearRefreshRate: 恢复默认")
} catch (e: Exception) {
android.util.Log.e("BatteryHealth", "clearRefreshRate 失败: ${e.message}")
}
}
override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) { override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine) super.configureFlutterEngine(flutterEngine)
// 充电状态通道
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHARGE_CHANNEL) MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHARGE_CHANNEL)
.setMethodCallHandler { call, result -> .setMethodCallHandler { call, result ->
if (call.method == "getChargeState") { if (call.method == "getChargeState") {
@@ -247,6 +275,7 @@ class MainActivity : FlutterActivity() {
} }
} }
// SAF 通道
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, SAF_CHANNEL) MethodChannel(flutterEngine.dartExecutor.binaryMessenger, SAF_CHANNEL)
.setMethodCallHandler { call, result -> .setMethodCallHandler { call, result ->
when (call.method) { when (call.method) {
@@ -283,6 +312,23 @@ class MainActivity : FlutterActivity() {
} }
} }
// 🟢 刷新率控制通道
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, FRAME_RATE_CHANNEL)
.setMethodCallHandler { call, result ->
when (call.method) {
"setHighRefreshRate" -> {
setHighRefreshRate()
result.success(true)
}
"clearRefreshRate" -> {
clearRefreshRate()
result.success(true)
}
else -> result.notImplemented()
}
}
// 事件通道
EventChannel(flutterEngine.dartExecutor.binaryMessenger, EVENT_CHANNEL) EventChannel(flutterEngine.dartExecutor.binaryMessenger, EVENT_CHANNEL)
.setStreamHandler( .setStreamHandler(
object : EventChannel.StreamHandler { object : EventChannel.StreamHandler {
+137
View File
@@ -0,0 +1,137 @@
## [1.6.0] - 2026-07-20
### ✨ 新增
- **魅族 21 Note 机型适配**:新增 `21 Note` 特征字识别,设计容量 5500mAh
- 21 Note 运行 Flyme 12.6 版本,日志格式与 21 系列一致
- 支持 `learned_cap` 字段直接读取,与 21 / 21 Pro 逻辑相同
- 已加入 `DeviceMatching` 机型映射表
- **更新日志页面**:内置 `CHANGELOG.md` 渲染,版本历史一目了然
- 使用 `flutter_markdown` 解析,支持标准 Markdown 格式
- 滚动进度条:实时显示阅读进度,提升浏览体验
- 进入页面自动请求 120Hz 高刷新率,滑动更流畅(Flyme LTPO 适配)
- 滚动时呼吸动画维持高刷感知,停止后自动恢复省电策略
- **AppBar 菜单重构**:刷新按钮左移,右侧新增三点菜单
- `更新日志`:已可用
- `深色模式`:占位(1.7.0 实现)
- `关于`:占位(1.7.0 实现)
- **原生层高刷控制**:新增 `MethodChannel` 通道,支持 Dart 侧动态申请/释放 120Hz
- 进入更新日志页面自动触发
- 退出页面自动释放,不影响系统省电策略
### ✅ 已确认完美支持(更新)
| 机型 | 容量来源 | 设计容量 |
|---|---|---|
| 魅族 20 / 20 Pro / 20 INFINITY | `remaining_cap` + `status=Full` | 4700 / 5000 / 4800 mAh |
| 魅族 21 / 21 Pro | `learned_cap` | 4800 / 5050 mAh |
| **魅族 21 Note(新增)** | `learned_cap` | **5500 mAh** |
### ⚡ 优化
- **UI 拆分**:将 `BatteryHealthPage` 的 UI 部分抽离至独立文件,降低耦合度
- **动画简化**:移除复合动画控制器(`AnimationController`),改用轻量 `AnimatedOpacity` 控制充电条目显隐
- **更新日志性能**:缓存 Markdown 渲染结果,避免重复解析
- **LTPO 适配**:通过进度条微动 + 原生层 `preferredRefreshRate`,解决更新日志页面降频卡顿问题
### 🐛 修复
- 修复更新日志页面因 `ListView` + `shrinkWrap` 导致的 `unbounded height` 渲染异常
- 修复充电条目动画状态残留导致的显示异常
- 修复部分 `TextStyle``color``foreground` 混用导致的渲染断言错误
### 📊 影响范围
- `device_matching.dart`:新增 21 Note 机型映射
- `battery_health_page.dart`AppBar 重构,动画简化
- `battery_data_layout.dart`(新增):UI 布局抽离
- `changelog_page.dart`(新增):更新日志页面
- `MainActivity.kt`:新增原生高刷控制通道
- `pubspec.yaml`:新增 `flutter_markdown` 依赖,添加 `assets/CHANGELOG.md`
### 📌 备注
- 本次更新为 1.6.0 封版版本,后续将聚焦 1.7.0(深色模式 + 关于页面)
- 21 Note 在 Flyme 12.6 上测试通过,日志解析正常 ✅
----
## [1.5.3] - 2026-07-19
- 新增:机型匹配优化
### ✅ 已确认完美支持
- 魅族 20 / 20 Pro / 20 INFINITY(基于remaining_cap + status=full联合判断)
- 魅族 21 / 21 Pro(直接读取learned_cap字段信息)
### ⚠️ 部分支持(测试中)
- 魅族 18 / 18s / 18 Pro / 18s Pro(日志存在,字段解析适配中)
- 魅族 16s(需回退原厂固件验证)
### ❌ 确认不支持
- 魅族 21 Note(星纪团队固件,日志格式乱码无法识别,导致异常)
- 魅族 22(系统未生成 `mbattery_charger` 目录)
### 📌 说明
- 适配状态仅针对 BMS 日志可读性,与手机硬件本身无关
- 部分旧机型(如 16 系)若无日志生成,则无法使用本工具
- 后续若发现其他机型可用,将持续补充
----
## [1.5.2] - 2026-07-14
### 新增
- 浪漫雅圆字体,工程内文件为FlymeFont.ttf
### 🎨 UI 优化
- **字体体系重构**:数字全局替换为 FlymeFont(浪漫雅圆),中文保留 HarmonyOS Sans SC
- 健康度百分比、容量数值、循环次数、电压/温度读数均使用 FlymeFont
- 中文/单位/符号保持 HarmonyOS Sans SC,确保阅读舒适度
- **温标符号修正**:温度显示由 `°C`(度符号 + C)改为专用字符 `℃`U+2103),与 FlymeFont 原生适配
- **容量行视觉权重优化**
- 当前容量与设计容量数字使用 FlymeFont(字号 20,字重 300
- `mAh` 单位改用 HarmonyOS Sans SC(字号 16,字重 500),与 `cycles` 保持一致
- 整行颜色统一为 `rgb(95, 95, 95)`,弱化视觉重量,突出健康度大数字
- **电压/温度极值显示优化**:数值与单位统一使用 FlymeFont,箭头符号保留 HarmonyOS
### 🐛 修复
- 修复 FlymeFont 数字 1 在部分场景下显示为红色的问题(字体内部颜色预设)
- 修复容量行 `designCap`(设计容量)未正确应用 FlymeFont 的问题
- 修复部分 `TextStyle``color``foreground` 混用导致的渲染断言错误
- 统一主界面与二级页面状态栏样式
### 📊 影响范围
- `battery_summary.dart`:健康度/循环/容量显示样式重构
- `battery_info_panel.dart`:电压/温度显示字体统一
- `charge_detail_page.dart`:数值字体统一
- `trend_chart.dart`:坐标轴数字字体统一
### 📌 备注
- 本次更新纯 UI 优化,不影响数据读取与计算逻辑
----
## [1.5.0] - 2026-07-09
### 🐛 修复
- 修复有线充电功率计算单位错误(µV × µA 计算时少除 1000,导致功率放大 1000 倍,属于潜藏bug,外面看不出问题)
- 修复魅族 21 系列 `learned_cap` 因受 `status=Full` 限制导致容量未及时更新的问题
- `learned_cap` 优先级提升:在所有状态行中优先匹配,不再局限于 `status=Full`
### ⚡ 优化
- 有线充电功率显示重构:
- 主界面:`≈{实际功率}w/{请求功率}w`(例:`≈30w/65w`
- 二级页面:拆分为“请求档位”(`≈80w/L-10`)和“实际功率”(`≈30w`)两行
- USB 慢充保持 `≈7.5w` 不变
- UI 统一:
- 统一主界面与二级页面状态栏背景色
- 统一主界面与二级页面 AppBar 背景色为 `Colors.white`
- 状态栏图标保持深色(`Brightness.dark`
## 更早期日志请移步至项目Git Release页面查看
Binary file not shown.
+3
View File
@@ -0,0 +1,3 @@
description: This file stores settings for Dart & Flutter DevTools.
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
extensions:
+37 -2
View File
@@ -5,7 +5,6 @@ import 'services/saf_storage_service.dart';
void main() async { void main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
// 🟢 初始化 SAF 服务
await SafStorageService().init(); await SafStorageService().init();
runApp(const MyApp()); runApp(const MyApp());
@@ -19,9 +18,45 @@ class MyApp extends StatelessWidget {
return MaterialApp( return MaterialApp(
title: '电池健康度', title: '电池健康度',
theme: ThemeData( theme: ThemeData(
// 🟢 中文用 HarmonyOS Sans SC
fontFamily: 'HarmonyOS_Sans_SC', fontFamily: 'HarmonyOS_Sans_SC',
// 🟢 数字回退到 FlymeFont
fontFamilyFallback: const ['Flyme'],
// 🟢 统一文字颜色为黑色
colorScheme: ColorScheme.fromSeed( colorScheme: ColorScheme.fromSeed(
seedColor: const Color.fromARGB(255, 255, 255, 255), seedColor: const Color(0xFFFFFFFF),
).copyWith(onSurface: Colors.black),
textTheme: const TextTheme(
// 大标题/数字(健康度、容量等)
displayMedium: TextStyle(
fontFamily: 'HarmonyOS_Sans_SC',
fontFamilyFallback: ['Flyme'],
color: Colors.black,
),
// 标题(如“容量百分比变动趋势”)
headlineMedium: TextStyle(
fontFamily: 'HarmonyOS_Sans_SC',
fontFamilyFallback: ['Flyme'],
color: Colors.black,
),
// 正文(电压、温度、循环次数等)
bodyMedium: TextStyle(
fontFamily: 'HarmonyOS_Sans_SC',
fontFamilyFallback: ['Flyme'],
color: Colors.black,
),
// 小字(日期、辅助信息)
bodySmall: TextStyle(
fontFamily: 'HarmonyOS_Sans_SC',
fontFamilyFallback: ['Flyme'],
color: Colors.black87,
),
// 数字专用(大号数字,如健康度百分比)
displayLarge: TextStyle(
fontFamily: 'HarmonyOS_Sans_SC',
fontFamilyFallback: ['Flyme'],
color: Colors.black,
),
), ),
useMaterial3: true, useMaterial3: true,
), ),
+4 -4
View File
@@ -2,11 +2,11 @@
class DeviceMatching { class DeviceMatching {
/// 根据机型返回设计容量(mAh) /// 根据机型返回设计容量(mAh)
static int getDesignCapacity(String model) { static int getDesignCapacity(String model) {
if (model.contains('22')) return 5510;
if (model.contains('21 Pro')) return 5050; if (model.contains('21 Pro')) return 5050;
if (model.contains('21 Note')) return 5500;
if (model.contains('21')) return 4800; if (model.contains('21')) return 4800;
if (model.contains('20 Pro')) return 5000; if (model.contains('20 Pro')) return 5000;
if (model.contains('20 INFINITY')) return 4800; if (model.contains('20 Inf')) return 4800;
if (model.contains('20')) return 4700; if (model.contains('20')) return 4700;
if (model.contains('18s Pro')) return 4500; if (model.contains('18s Pro')) return 4500;
if (model.contains('18s')) return 4000; if (model.contains('18s')) return 4000;
@@ -29,11 +29,11 @@ class DeviceMatching {
/// 根据机型返回显示名称 /// 根据机型返回显示名称
static String getModelName(String model) { static String getModelName(String model) {
if (model.contains('22')) return '魅族22';
if (model.contains('21 Pro')) return '魅族21 Pro'; if (model.contains('21 Pro')) return '魅族21 Pro';
if (model.contains('21 Note')) return '魅族21 Note';
if (model.contains('21')) return '魅族21'; if (model.contains('21')) return '魅族21';
if (model.contains('20 Pro')) return '魅族20 Pro'; if (model.contains('20 Pro')) return '魅族20 Pro';
if (model.contains('20 INFINITY')) return '魅族20 INFINITY'; if (model.contains('20 Inf')) return '魅族20 Infinity';
if (model.contains('20')) return '魅族20'; if (model.contains('20')) return '魅族20';
if (model.contains('18s Pro')) return '魅族18s Pro'; if (model.contains('18s Pro')) return '魅族18s Pro';
if (model.contains('18s')) return '魅族18s'; if (model.contains('18s')) return '魅族18s';
+104 -131
View File
@@ -4,21 +4,22 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:device_info_plus/device_info_plus.dart'; import 'package:device_info_plus/device_info_plus.dart';
// 检查这些导入是否正确 // --- 本地导入 ---
import '../models/device_matching.dart'; // ✅ 存在 import '../models/device_matching.dart';
import '../models/saf_permission_state.dart'; // ✅ 存在 import '../models/saf_permission_state.dart';
import '../services/database_service.dart'; import '../services/database_service.dart';
import '../services/battery_data_service.dart'; import '../services/battery_data_service.dart';
import '../services/saf_storage_service.dart'; // ✅ 存在 import '../services/saf_storage_service.dart';
import '../logic/charge_parser.dart'; // ✅ 存在 import '../logic/charge_parser.dart';
import '../logic/capacity_extractor.dart'; // ✅ 存在(注意:不是 utils/) import '../logic/capacity_extractor.dart';
import '../utils/time_utils.dart'; // ✅ 存在 import '../utils/time_utils.dart';
import '../utils/charge_utils.dart'; // ✅ 存在 import '../utils/charge_utils.dart';
import '../widgets/trend_chart.dart'; // ⚠️ 需要确认文件内容完整 import '../widgets/battery_summary.dart';
import '../widgets/battery_summary.dart'; // ⚠️ 需要确认文件内容完整 import '../widgets/battery_info_panel.dart';
import '../widgets/battery_info_panel.dart'; // ⚠️ 需要确认文件内容完整 import '../widgets/charge_entry.dart';
import '../widgets/charge_entry.dart'; // ⚠️ 需要确认文件内容完整 import '../widgets/charge_detail_page.dart';
import '../widgets/charge_detail_page.dart'; // ✅ 存在 import '../widgets/trend_chart.dart';
import '../pages/changelog_page.dart';
// --- 潘通色定义 --- // --- 潘通色定义 ---
const Color kHealthGreen = Color(0xFF00B140); const Color kHealthGreen = Color(0xFF00B140);
@@ -32,8 +33,7 @@ class BatteryHealthPage extends StatefulWidget {
State<BatteryHealthPage> createState() => _BatteryHealthPageState(); State<BatteryHealthPage> createState() => _BatteryHealthPageState();
} }
class _BatteryHealthPageState extends State<BatteryHealthPage> class _BatteryHealthPageState extends State<BatteryHealthPage> {
with SingleTickerProviderStateMixin {
// ---------- 数据 ---------- // ---------- 数据 ----------
double _currentCap = 0; double _currentCap = 0;
int _cycleCount = 0; int _cycleCount = 0;
@@ -66,11 +66,6 @@ class _BatteryHealthPageState extends State<BatteryHealthPage>
final BatteryDataService _dataService = BatteryDataService(); final BatteryDataService _dataService = BatteryDataService();
SafPermissionState _safState = SafPermissionState.unauthorized; SafPermissionState _safState = SafPermissionState.unauthorized;
// 动画控制
late AnimationController _chargeAnimationController;
late Animation<double> _bottomSlide;
late Animation<double> _entryFadeIn;
// 平台通道 // 平台通道
static const _methodChannel = MethodChannel('com.lxh.battery_health/charge'); static const _methodChannel = MethodChannel('com.lxh.battery_health/charge');
static const _eventChannel = EventChannel( static const _eventChannel = EventChannel(
@@ -87,8 +82,6 @@ class _BatteryHealthPageState extends State<BatteryHealthPage>
super.initState(); super.initState();
dev.log('🔵 ========== initState 开始 ==========', name: 'BatteryHealth'); dev.log('🔵 ========== initState 开始 ==========', name: 'BatteryHealth');
_initAnimations();
_getInitialChargeState(); _getInitialChargeState();
_chargeEventSubscription = _eventChannel.receiveBroadcastStream().listen(( _chargeEventSubscription = _eventChannel.receiveBroadcastStream().listen((
@@ -134,31 +127,10 @@ class _BatteryHealthPageState extends State<BatteryHealthPage>
void dispose() { void dispose() {
_chargeEventSubscription?.cancel(); _chargeEventSubscription?.cancel();
_pollTimer?.cancel(); _pollTimer?.cancel();
_chargeAnimationController.dispose();
_dbService.database.then((db) => db.close()).ignore(); _dbService.database.then((db) => db.close()).ignore();
super.dispose(); super.dispose();
} }
// ---------- 动画初始化 ----------
void _initAnimations() {
_chargeAnimationController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 500),
);
_bottomSlide = Tween<double>(begin: 0, end: 24).animate(
CurvedAnimation(
parent: _chargeAnimationController,
curve: Curves.easeInOutCubic,
),
);
_entryFadeIn = Tween<double>(begin: 0, end: 1).animate(
CurvedAnimation(
parent: _chargeAnimationController,
curve: const Interval(0.0, 0.5, curve: Curves.easeIn),
),
);
}
// ---------- 设备信息 ---------- // ---------- 设备信息 ----------
Future<void> _getDeviceInfo() async { Future<void> _getDeviceInfo() async {
dev.log('🔵 _getDeviceInfo 开始', name: 'BatteryHealth'); dev.log('🔵 _getDeviceInfo 开始', name: 'BatteryHealth');
@@ -275,7 +247,6 @@ class _BatteryHealthPageState extends State<BatteryHealthPage>
_chargeCurr = 0.0; _chargeCurr = 0.0;
_wiredLevel = null; _wiredLevel = null;
_chargingStartTime = null; _chargingStartTime = null;
_updateChargeState();
return; return;
} }
@@ -310,7 +281,6 @@ class _BatteryHealthPageState extends State<BatteryHealthPage>
_chargePower = 0.0; _chargePower = 0.0;
_chargeVolt = 0.0; _chargeVolt = 0.0;
_chargeCurr = 0.0; _chargeCurr = 0.0;
_updateChargeState();
}); });
if (_isCharging) { if (_isCharging) {
@@ -349,7 +319,6 @@ class _BatteryHealthPageState extends State<BatteryHealthPage>
_currentCap = recent.first['capacity'].toDouble(); _currentCap = recent.first['capacity'].toDouble();
_maxVolt = recent.first['volt_max'] ?? 0; _maxVolt = recent.first['volt_max'] ?? 0;
_minVolt = recent.first['volt_min'] ?? 0; _minVolt = recent.first['volt_min'] ?? 0;
// 🟢 数据库目前不存温度,后续升级可扩展,暂时保留为 0
_maxTemp = 0; _maxTemp = 0;
_minTemp = 0; _minTemp = 0;
_cycleCount = 0; _cycleCount = 0;
@@ -361,8 +330,6 @@ class _BatteryHealthPageState extends State<BatteryHealthPage>
setState(() => _hasData = false); setState(() => _hasData = false);
} }
if (_isCharging) _fetchChargeDataFromLog(); if (_isCharging) _fetchChargeDataFromLog();
_chargeAnimationController.reset();
_updateChargeState();
return; return;
} }
@@ -374,23 +341,16 @@ class _BatteryHealthPageState extends State<BatteryHealthPage>
dev.log('🔵 文件为空', name: 'BatteryHealth'); dev.log('🔵 文件为空', name: 'BatteryHealth');
setState(() => _hasData = false); setState(() => _hasData = false);
if (_isCharging) _fetchChargeDataFromLog(); if (_isCharging) _fetchChargeDataFromLog();
_chargeAnimationController.reset();
_updateChargeState();
return; return;
} }
// 🟢 提取所有有效行(包含 tbat=)
final validLines = lines.where((line) => line.contains('tbat=')).toList(); final validLines = lines.where((line) => line.contains('tbat=')).toList();
if (validLines.isEmpty) { if (validLines.isEmpty) {
dev.log('🔵 无有效数据行', name: 'BatteryHealth'); dev.log('🔵 无有效数据行', name: 'BatteryHealth');
setState(() => _hasData = false); setState(() => _hasData = false);
_chargeAnimationController.reset();
_updateChargeState();
return; return;
} }
// 🟢 1.4.7 核心改动:字段提取与容量提取解耦
// 无论 cap 是否为 null,都提取常规字段
final lastLine = validLines.last; final lastLine = validLines.last;
final cycle = _extractCycle(lastLine); final cycle = _extractCycle(lastLine);
final temp = _extractTemp(lastLine); final temp = _extractTemp(lastLine);
@@ -403,16 +363,13 @@ class _BatteryHealthPageState extends State<BatteryHealthPage>
name: 'BatteryHealth', name: 'BatteryHealth',
); );
// 🟢 容量提取(可能为 null)
final cap = CapacityExtractor.extractCapacity(lines); final cap = CapacityExtractor.extractCapacity(lines);
int? finalCap; int? finalCap;
if (cap != null) { if (cap != null) {
// ✅ 今日有充满记录,直接使用
finalCap = cap; finalCap = cap;
dev.log('🔵 从日志提取容量: $cap', name: 'BatteryHealth'); dev.log('🔵 从日志提取容量: $cap', name: 'BatteryHealth');
} else { } else {
// ⚠️ 今日无充满记录,尝试从数据库复制前一天容量
dev.log('🔵 今日无充满记录,尝试复制前一天容量', name: 'BatteryHealth'); dev.log('🔵 今日无充满记录,尝试复制前一天容量', name: 'BatteryHealth');
final recent = await _dbService.queryRecentData(1); final recent = await _dbService.queryRecentData(1);
if (recent.isNotEmpty) { if (recent.isNotEmpty) {
@@ -424,7 +381,6 @@ class _BatteryHealthPageState extends State<BatteryHealthPage>
} }
} }
// 🟢 一次性 setState 更新所有字段
setState(() { setState(() {
_currentCap = (finalCap ?? _currentCap).toDouble(); _currentCap = (finalCap ?? _currentCap).toDouble();
_cycleCount = cycle; _cycleCount = cycle;
@@ -443,8 +399,6 @@ class _BatteryHealthPageState extends State<BatteryHealthPage>
} }
await _updateTrendChart(); await _updateTrendChart();
_chargeAnimationController.reset();
_updateChargeState();
dev.log( dev.log(
'🔵 ========== readBatteryCapacity 完成 ==========', '🔵 ========== readBatteryCapacity 完成 ==========',
name: 'BatteryHealth', name: 'BatteryHealth',
@@ -452,8 +406,6 @@ class _BatteryHealthPageState extends State<BatteryHealthPage>
} catch (e) { } catch (e) {
dev.log('🔵 readBatteryCapacity 异常: $e', name: 'BatteryHealth'); dev.log('🔵 readBatteryCapacity 异常: $e', name: 'BatteryHealth');
setState(() => _hasData = false); setState(() => _hasData = false);
_chargeAnimationController.reset();
_updateChargeState();
} }
} }
@@ -587,33 +539,12 @@ class _BatteryHealthPageState extends State<BatteryHealthPage>
} }
} }
// ---------- 动画控制 ----------
void _startChargeAnimation() {
if (_chargeAnimationController.isAnimating) return;
_chargeAnimationController.forward();
}
void _stopChargeAnimation() {
if (_chargeAnimationController.isAnimating) return;
_chargeAnimationController.reverse();
}
void _updateChargeState() {
if (!mounted) return;
if (_isCharging) {
_startChargeAnimation();
} else {
_stopChargeAnimation();
}
}
// ---------- 显示辅助 ---------- // ---------- 显示辅助 ----------
String _getDisplayPower() { String _getDisplayPower() {
if (!_isCharging) return ''; if (!_isCharging) return '';
if (_isWireless) { if (_isWireless) {
return ChargeUtils.getDisplayPowerWireless(_chargePower); return ChargeUtils.getDisplayPowerWireless(_chargePower);
} else { } else {
// 🟢 1.5.0 改用新格式:实际/标定
return ChargeUtils.getMainDisplayPowerWired( return ChargeUtils.getMainDisplayPowerWired(
_chargePower, _chargePower,
_wiredLevel, _wiredLevel,
@@ -629,9 +560,16 @@ class _BatteryHealthPageState extends State<BatteryHealthPage>
// ---------- Build ---------- // ---------- Build ----------
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final double healthPercent = (_designCap > 0)
? (_currentCap / _designCap) * 100
: 0.0;
final String chargeSummary = _isCharging
? '充电中 · ${_getChargeTypeLabel()} · ${_getDisplayPower()}'
: '';
return AnnotatedRegion<SystemUiOverlayStyle>( return AnnotatedRegion<SystemUiOverlayStyle>(
value: const SystemUiOverlayStyle( value: const SystemUiOverlayStyle(
// 🟢 1.5.0 统一状态栏颜色
statusBarColor: Color.fromRGBO(157, 212, 237, 0.1), statusBarColor: Color.fromRGBO(157, 212, 237, 0.1),
statusBarIconBrightness: Brightness.dark, statusBarIconBrightness: Brightness.dark,
statusBarBrightness: Brightness.dark, statusBarBrightness: Brightness.dark,
@@ -659,42 +597,52 @@ class _BatteryHealthPageState extends State<BatteryHealthPage>
tooltip: '刷新数据', tooltip: '刷新数据',
color: Colors.black, color: Colors.black,
), ),
], PopupMenuButton<String>(
), icon: const Icon(Icons.more_vert),
body: _hasData ? _buildDataLayout() : _buildEmptyState(), color: Colors.white,
onSelected: (value) {
switch (value) {
case 'changelog':
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const ChangelogPage(),
), ),
); );
break;
case 'dark_mode':
break;
case 'about':
break;
} }
},
Widget _buildEmptyState() { itemBuilder: (context) => [
return const Center( const PopupMenuItem(value: 'changelog', child: Text('更新日志')),
child: Column( const PopupMenuItem(
mainAxisAlignment: MainAxisAlignment.center, value: 'dark_mode',
enabled: false,
child: Text('深色模式(开发中)'),
),
const PopupMenuItem(
value: 'about',
enabled: false,
child: Text('关于(开发中)'),
),
],
),
],
),
body: _hasData
? Column(
children: [ children: [
Icon(Icons.battery_unknown, size: 64, color: Colors.grey), // 🟢 可滚动区域(带 RepaintBoundary
SizedBox(height: 16), Expanded(
Text('暂无数据', style: TextStyle(fontSize: 18, color: Colors.grey)), child: SingleChildScrollView(
Text('点击右上角刷新尝试', style: TextStyle(fontSize: 14, color: Colors.grey)),
],
),
);
}
Widget _buildDataLayout() {
final healthPercent = (_designCap > 0)
? (_currentCap / _designCap) * 100
: 0;
String chargeSummary = '';
if (_isCharging) {
chargeSummary = '充电中 · ${_getChargeTypeLabel()} · ${_getDisplayPower()}';
}
return Padding(
padding: const EdgeInsets.all(20.0), padding: const EdgeInsets.all(20.0),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// 设备型号
Text( Text(
_deviceModel, _deviceModel,
style: const TextStyle( style: const TextStyle(
@@ -705,25 +653,33 @@ class _BatteryHealthPageState extends State<BatteryHealthPage>
), ),
const SizedBox(height: 4), const SizedBox(height: 4),
BatterySummary( // 🟢 BatterySummary 用 RepaintBoundary 隔离
healthPercent: healthPercent.toDouble(), // 显式转换 RepaintBoundary(
child: BatterySummary(
healthPercent: healthPercent,
cycleCount: _cycleCount, cycleCount: _cycleCount,
currentCap: _currentCap, currentCap: _currentCap,
designCap: _designCap, designCap: _designCap,
), ),
),
const SizedBox(height: 24), const SizedBox(height: 24),
TrendChart(data: _trendData, designCap: _designCap), // 🟢 TrendChart 用 RepaintBoundary 隔离
RepaintBoundary(
child: TrendChart(
data: _trendData,
designCap: _designCap,
),
),
const SizedBox(height: 8), const SizedBox(height: 8),
AnimatedBuilder( // 🟢 充电条目 - 保留 AnimatedOpacity,但加 RepaintBoundary
animation: _chargeAnimationController, RepaintBoundary(
builder: (context, child) { child: AnimatedOpacity(
final entryHeight = 28 * _entryFadeIn.value; opacity: _isCharging ? 1.0 : 0.0,
final chargeEntry = Opacity( duration: const Duration(milliseconds: 300),
opacity: _entryFadeIn.value,
child: ChargeEntry( child: ChargeEntry(
summary: chargeSummary, summary: chargeSummary,
isCharging: _isCharging, isCharging: _isCharging,
@@ -745,13 +701,13 @@ class _BatteryHealthPageState extends State<BatteryHealthPage>
} }
}, },
), ),
); ),
),
return Column( const SizedBox(height: 4),
children: [
SizedBox(height: entryHeight, child: chargeEntry), // 🟢 BatteryInfoPanel 也用 RepaintBoundary
Transform.translate( RepaintBoundary(
offset: Offset(0, _bottomSlide.value),
child: BatteryInfoPanel( child: BatteryInfoPanel(
batteryVolt: _batteryVolt, batteryVolt: _batteryVolt,
batteryTemp: _batteryTemp, batteryTemp: _batteryTemp,
@@ -762,11 +718,28 @@ class _BatteryHealthPageState extends State<BatteryHealthPage>
), ),
), ),
], ],
);
},
), ),
const Spacer(), ),
),
], ],
)
: const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.battery_unknown, size: 64, color: Colors.grey),
SizedBox(height: 16),
Text(
'暂无数据',
style: TextStyle(fontSize: 18, color: Colors.grey),
),
Text(
'点击右上角刷新尝试',
style: TextStyle(fontSize: 14, color: Colors.grey),
),
],
),
),
), ),
); );
} }
+205
View File
@@ -0,0 +1,205 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; // 🟢 添加这一行
import 'package:flutter/services.dart' show rootBundle;
import 'package:flutter_markdown/flutter_markdown.dart';
class ChangelogPage extends StatefulWidget {
const ChangelogPage({super.key});
@override
State<ChangelogPage> createState() => _ChangelogPageState();
}
class _ChangelogPageState extends State<ChangelogPage>
with SingleTickerProviderStateMixin {
// ---------- 数据 ----------
String _markdownContent = '';
bool _isLoading = true;
String? _error;
Widget? _cachedMarkdown;
// ---------- 滚动 ----------
final ScrollController _scrollController = ScrollController();
double _scrollProgress = 0.0;
bool _isScrolling = false;
// ---------- 呼吸动画(维持高刷感知) ----------
late AnimationController _breathController;
late Animation<double> _breathAnimation;
// ---------- 原生刷新率控制 ----------
// 🟢 改用 static final(不是 const
static final _frameRateChannel = MethodChannel(
'com.lxh.battery_health/frame_rate',
);
// ---------- 生命周期 ----------
@override
void initState() {
super.initState();
_loadChangelog();
// 进入页面时申请高刷(原生层)
_frameRateChannel.invokeMethod('setHighRefreshRate');
// 呼吸动画:让进度条在滚动时轻微抖动,维持高刷感知
_breathController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 500),
)..repeat(reverse: true);
_breathAnimation = Tween<double>(begin: 0.0, end: 0.03).animate(
CurvedAnimation(parent: _breathController, curve: Curves.easeInOut),
);
_scrollController.addListener(() {
_updateProgress();
final isScrolling = _scrollController.position.isScrollingNotifier.value;
if (isScrolling != _isScrolling) {
setState(() {
_isScrolling = isScrolling;
});
if (isScrolling) {
_breathController.repeat(reverse: true);
} else {
_breathController.stop();
_breathController.reset();
}
}
});
}
@override
void dispose() {
// 离开页面时恢复默认刷新率
_frameRateChannel.invokeMethod('clearRefreshRate');
_scrollController.removeListener(_updateProgress);
_scrollController.dispose();
_breathController.dispose();
super.dispose();
}
// ---------- 方法 ----------
void _updateProgress() {
if (!_scrollController.hasClients) return;
final maxScroll = _scrollController.position.maxScrollExtent;
final currentScroll = _scrollController.position.pixels;
final progress = maxScroll > 0 ? currentScroll / maxScroll : 0.0;
setState(() {
_scrollProgress = progress;
});
}
Future<void> _loadChangelog() async {
try {
final content = await rootBundle.loadString('assets/CHANGELOG.md');
setState(() {
_markdownContent = content;
_isLoading = false;
_cachedMarkdown = _buildMarkdown(content);
});
} catch (e) {
setState(() {
_error = '无法加载更新日志: $e';
_isLoading = false;
});
}
}
Widget _buildMarkdown(String content) {
return Markdown(
data: content,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
styleSheet: MarkdownStyleSheet(
h1: const TextStyle(
fontFamily: 'HarmonyOS_Sans_SC',
fontSize: 24,
fontWeight: FontWeight.w700,
color: Colors.black,
),
h2: const TextStyle(
fontFamily: 'HarmonyOS_Sans_SC',
fontSize: 20,
fontWeight: FontWeight.w600,
color: Colors.black87,
),
h3: const TextStyle(
fontFamily: 'HarmonyOS_Sans_SC',
fontSize: 18,
fontWeight: FontWeight.w600,
color: Colors.black87,
),
p: const TextStyle(
fontFamily: 'HarmonyOS_Sans_SC',
fontSize: 16,
color: Colors.black87,
),
listBullet: const TextStyle(
fontFamily: 'HarmonyOS_Sans_SC',
fontSize: 16,
color: Colors.black87,
),
code: const TextStyle(
fontFamily: 'HarmonyOS_Sans_SC',
fontSize: 14,
color: Colors.black87,
backgroundColor: Color(0xFFF5F5F5),
),
),
);
}
// ---------- Build ----------
@override
Widget build(BuildContext context) {
final breathOffset = _isScrolling ? _breathAnimation.value : 0.0;
final displayProgress = (_scrollProgress + breathOffset).clamp(0.0, 1.0);
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
title: const Text(
'更新日志',
style: TextStyle(
fontFamily: 'HarmonyOS_Sans_SC',
color: Colors.black,
),
),
backgroundColor: Colors.white,
elevation: 0,
foregroundColor: Colors.black,
iconTheme: const IconThemeData(color: Colors.black),
bottom: PreferredSize(
preferredSize: const Size.fromHeight(2.0),
child: LinearProgressIndicator(
value: displayProgress,
backgroundColor: Colors.grey.shade200,
valueColor: const AlwaysStoppedAnimation<Color>(
Color.fromRGBO(157, 212, 237, 1.0),
),
minHeight: 2.0,
),
),
),
body: _isLoading
? const Center(child: CircularProgressIndicator())
: _error != null
? Center(
child: Text(
_error!,
style: const TextStyle(
fontFamily: 'HarmonyOS_Sans_SC',
color: Colors.grey,
),
),
)
: SingleChildScrollView(
controller: _scrollController,
padding: const EdgeInsets.all(16.0),
child: _cachedMarkdown ?? const SizedBox.shrink(),
),
);
}
}
+151
View File
@@ -0,0 +1,151 @@
import 'package:flutter/material.dart';
import '../widgets/trend_chart.dart';
import '../widgets/battery_summary.dart';
import '../widgets/battery_info_panel.dart';
import '../widgets/charge_entry.dart';
import '../widgets/charge_detail_page.dart';
/// 电池数据布局(含数据展示、趋势图、充电状态、电压温度面板)
class BatteryDataLayout extends StatelessWidget {
final String deviceModel;
final double healthPercent;
final int cycleCount;
final double currentCap;
final int designCap;
final List<Map<String, dynamic>> trendData;
final bool isCharging;
final String chargeSummary;
final double batteryVolt;
final double batteryTemp;
final double maxVolt;
final double minVolt;
final double maxTemp;
final double minTemp;
final String chargeType;
final bool isWireless;
final double chargePower;
final double chargeVolt;
final double chargeCurr;
final int? wiredLevel;
const BatteryDataLayout({
super.key,
required this.deviceModel,
required this.healthPercent,
required this.cycleCount,
required this.currentCap,
required this.designCap,
required this.trendData,
required this.isCharging,
required this.chargeSummary,
required this.batteryVolt,
required this.batteryTemp,
required this.maxVolt,
required this.minVolt,
required this.maxTemp,
required this.minTemp,
required this.chargeType,
required this.isWireless,
required this.chargePower,
required this.chargeVolt,
required this.chargeCurr,
this.wiredLevel,
});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
deviceModel,
style: const TextStyle(
fontSize: 14,
color: Colors.grey,
fontWeight: FontWeight.w400,
),
),
const SizedBox(height: 4),
BatterySummary(
healthPercent: healthPercent,
cycleCount: cycleCount,
currentCap: currentCap,
designCap: designCap,
),
const SizedBox(height: 24),
TrendChart(data: trendData, designCap: designCap),
const SizedBox(height: 8),
// 充电状态 + 信息面板(带进场动画)
_buildChargeAndInfoPanel(context),
const Spacer(),
],
),
);
}
Widget _buildChargeAndInfoPanel(BuildContext context) {
return Column(
children: [
// 充电状态入口(保持原有的透明度/位移动画由父级控制)
ChargeEntry(
summary: chargeSummary,
isCharging: isCharging,
onTap: () {
if (isCharging) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ChargeDetailPage(
chargeType: chargeType,
isWireless: isWireless,
power: chargePower,
volt: chargeVolt,
curr: chargeCurr,
wiredLevel: wiredLevel,
),
),
);
}
},
),
const SizedBox(height: 4),
BatteryInfoPanel(
batteryVolt: batteryVolt,
batteryTemp: batteryTemp,
maxVolt: maxVolt,
minVolt: minVolt,
maxTemp: maxTemp,
minTemp: minTemp,
),
],
);
}
}
/// 空状态布局(无数据时显示)
class EmptyStateLayout extends StatelessWidget {
const EmptyStateLayout({super.key});
@override
Widget build(BuildContext context) {
return const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.battery_unknown, size: 64, color: Colors.grey),
SizedBox(height: 16),
Text('暂无数据', style: TextStyle(fontSize: 18, color: Colors.grey)),
Text('点击右上角刷新尝试', style: TextStyle(fontSize: 14, color: Colors.grey)),
],
),
);
}
}
+79 -20
View File
@@ -1,7 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_svg/flutter_svg.dart';
/// 电池信息面板(电压 + 温度 + 极值)
class BatteryInfoPanel extends StatelessWidget { class BatteryInfoPanel extends StatelessWidget {
final double batteryVolt; final double batteryVolt;
final double batteryTemp; final double batteryTemp;
@@ -37,21 +36,45 @@ class BatteryInfoPanel extends StatelessWidget {
const SizedBox(height: 8), const SizedBox(height: 8),
Row( Row(
children: [ children: [
Text( RichText(
'${maxVolt.toStringAsFixed(3)}V', text: TextSpan(
style: const TextStyle( style: TextStyle(
color: Colors.red, fontFamily: 'Flyme',
fontSize: 14, fontSize: 14,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
color: Colors.red,
),
children: [
const TextSpan(
text: '',
style: TextStyle(
fontFamily: 'HarmonyOS_Sans_SC',
color: Colors.red,
),
),
TextSpan(text: '${maxVolt.toStringAsFixed(3)}V'),
],
), ),
), ),
const SizedBox(width: 12), const SizedBox(width: 12),
Text( RichText(
'${minVolt.toStringAsFixed(3)}V', text: TextSpan(
style: const TextStyle( style: TextStyle(
color: Colors.blue, fontFamily: 'Flyme',
fontSize: 14, fontSize: 14,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
color: Colors.blue,
),
children: [
const TextSpan(
text: '',
style: TextStyle(
fontFamily: 'HarmonyOS_Sans_SC',
color: Colors.blue,
),
),
TextSpan(text: '${minVolt.toStringAsFixed(3)}V'),
],
), ),
), ),
], ],
@@ -66,26 +89,50 @@ class BatteryInfoPanel extends StatelessWidget {
_buildInfoItem( _buildInfoItem(
_buildIcon('temperature'), _buildIcon('temperature'),
'温度', '温度',
'${batteryTemp.toStringAsFixed(0)}°C', '${batteryTemp.toStringAsFixed(0)}',
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
Row( Row(
children: [ children: [
Text( RichText(
'${maxTemp.toStringAsFixed(0)}°C', text: TextSpan(
style: const TextStyle( style: TextStyle(
color: Colors.red, fontFamily: 'Flyme',
fontSize: 14, fontSize: 14,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
color: Colors.red,
),
children: [
const TextSpan(
text: '',
style: TextStyle(
fontFamily: 'HarmonyOS_Sans_SC',
color: Colors.red,
),
),
TextSpan(text: '${maxTemp.toStringAsFixed(0)}'),
],
), ),
), ),
const SizedBox(width: 12), const SizedBox(width: 12),
Text( RichText(
'${minTemp.toStringAsFixed(0)}°C', text: TextSpan(
style: const TextStyle( style: TextStyle(
color: Colors.blue, fontFamily: 'Flyme',
fontSize: 14, fontSize: 14,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
color: Colors.blue,
),
children: [
const TextSpan(
text: '',
style: TextStyle(
fontFamily: 'HarmonyOS_Sans_SC',
color: Colors.blue,
),
),
TextSpan(text: '${minTemp.toStringAsFixed(0)}'),
],
), ),
), ),
], ],
@@ -123,11 +170,23 @@ class BatteryInfoPanel extends StatelessWidget {
children: [ children: [
icon, icon,
const SizedBox(width: 6), const SizedBox(width: 6),
Text(label, style: const TextStyle(fontSize: 14, color: Colors.grey)), Text(
label,
style: const TextStyle(
fontFamily: 'HarmonyOS_Sans_SC',
fontSize: 14,
color: Colors.grey,
),
),
const SizedBox(width: 6), const SizedBox(width: 6),
Text( Text(
value, value,
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500), style: const TextStyle(
fontFamily: 'Flyme',
fontSize: 16,
fontWeight: FontWeight.w500,
color: Colors.black,
),
), ),
], ],
); );
+43 -29
View File
@@ -1,6 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
/// 电池健康度摘要组件(健康度 + cycles + mAh
class BatterySummary extends StatelessWidget { class BatterySummary extends StatelessWidget {
final double healthPercent; final double healthPercent;
final int cycleCount; final int cycleCount;
@@ -17,6 +16,8 @@ class BatterySummary extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final bool isLowHealth = healthPercent < 70;
return Row( return Row(
crossAxisAlignment: CrossAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.end,
children: [ children: [
@@ -28,20 +29,22 @@ class BatterySummary extends StatelessWidget {
Text( Text(
healthPercent.toStringAsFixed(1), healthPercent.toStringAsFixed(1),
style: TextStyle( style: TextStyle(
fontFamily: 'Flyme',
fontSize: 40, fontSize: 40,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
height: 1.0, height: 1.0,
color: healthPercent < 70 ? Colors.red : Colors.black, color: isLowHealth ? Colors.red : Colors.black,
), ),
), ),
const SizedBox(width: 2), const SizedBox(width: 2),
Text( Text(
'%', '%',
style: TextStyle( style: TextStyle(
fontFamily: 'Flyme',
fontSize: 20, fontSize: 20,
fontWeight: FontWeight.w300, fontWeight: FontWeight.w300,
height: 1.0, height: 1.0,
color: healthPercent < 70 ? Colors.red : Colors.grey.shade600, color: isLowHealth ? Colors.red : Colors.grey.shade600,
), ),
), ),
], ],
@@ -51,46 +54,57 @@ class BatterySummary extends StatelessWidget {
Column( Column(
crossAxisAlignment: CrossAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.end,
children: [ children: [
Text( RichText(
'$cycleCount cycles', // ✅ 修正:去掉下划线,使用 cycleCount text: TextSpan(
style: const TextStyle( style: TextStyle(
fontFamily: 'Flyme',
fontSize: 16, fontSize: 16,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
height: 1.2, height: 1.2,
color: Colors.black,
), ),
),
const SizedBox(height: 4),
Text.rich(
TextSpan(
children: [ children: [
TextSpan(text: '$cycleCount'),
TextSpan( TextSpan(
text: currentCap.toStringAsFixed(0), text: ' cycles',
style: const TextStyle( style: const TextStyle(
fontSize: 20, fontFamily: 'HarmonyOS_Sans_SC',
fontWeight: FontWeight.w300, color: Colors.black,
),
),
TextSpan(
text: ' / ',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w300,
color: Colors.grey.shade500,
),
),
TextSpan(
text: '${designCap}mAh',
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.w300,
), ),
), ),
], ],
), ),
style: const TextStyle( ),
const SizedBox(height: 4),
RichText(
text: TextSpan(
style: TextStyle(
fontFamily: 'Flyme',
fontSize: 20, fontSize: 20,
fontWeight: FontWeight.w300, fontWeight: FontWeight.w300,
height: 1.0, height: 1.0,
color: const Color(0xFF5F5F5F),
),
children: [
TextSpan(text: currentCap.toStringAsFixed(0)),
TextSpan(
text: ' / ',
style: TextStyle(
fontFamily: 'HarmonyOS_Sans_SC',
color: const Color(0xFF5F5F5F).withValues(alpha: 0.5),
),
),
TextSpan(text: designCap.toString()),
TextSpan(
text: ' mAh',
style: const TextStyle(
fontFamily: 'HarmonyOS_Sans_SC',
fontSize: 16,
fontWeight: FontWeight.w500,
color: Color(0xFF5F5F5F),
),
),
],
), ),
), ),
], ],
+61 -22
View File
@@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import '../utils/charge_utils.dart'; import '../utils/charge_utils.dart';
/// 充电详情页
class ChargeDetailPage extends StatelessWidget { class ChargeDetailPage extends StatelessWidget {
final String chargeType; final String chargeType;
final bool isWireless; final bool isWireless;
@@ -12,14 +11,14 @@ class ChargeDetailPage extends StatelessWidget {
final int? wiredLevel; final int? wiredLevel;
const ChargeDetailPage({ const ChargeDetailPage({
Key? key, super.key,
required this.chargeType, required this.chargeType,
required this.isWireless, required this.isWireless,
required this.power, required this.power,
required this.volt, required this.volt,
required this.curr, required this.curr,
this.wiredLevel, this.wiredLevel,
}) : super(key: key); });
String _getProtocolDisplay() { String _getProtocolDisplay() {
if (isWireless) return '无线充电($chargeType'; if (isWireless) return '无线充电($chargeType';
@@ -43,9 +42,8 @@ class ChargeDetailPage extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AnnotatedRegion<SystemUiOverlayStyle>( return AnnotatedRegion<SystemUiOverlayStyle>(
// 🟢 1.5.0 与主界面统一状态栏样式
value: const SystemUiOverlayStyle( value: const SystemUiOverlayStyle(
statusBarColor: Color.fromRGBO(157, 212, 237, 0.5), statusBarColor: Color.fromRGBO(157, 212, 237, 0.1),
statusBarIconBrightness: Brightness.dark, statusBarIconBrightness: Brightness.dark,
statusBarBrightness: Brightness.dark, statusBarBrightness: Brightness.dark,
), ),
@@ -53,7 +51,7 @@ class ChargeDetailPage extends StatelessWidget {
backgroundColor: Colors.white, backgroundColor: Colors.white,
appBar: AppBar( appBar: AppBar(
title: const Text('充电详情'), title: const Text('充电详情'),
backgroundColor: Colors.white, // 🟢 与主界面统一 backgroundColor: Colors.white,
elevation: 0, elevation: 0,
scrolledUnderElevation: 0, scrolledUnderElevation: 0,
shadowColor: Colors.transparent, shadowColor: Colors.transparent,
@@ -66,38 +64,36 @@ class ChargeDetailPage extends StatelessWidget {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
_buildDetailRow('协议', _getProtocolDisplay()), _buildTextRow('协议', _getProtocolDisplay()),
const Divider(), const Divider(),
// 有线充电:标定档位 + 实际功率
if (!isWireless) ...[ if (!isWireless) ...[
_buildDetailRow('标定档位', _getRequestedDisplay()), _buildTextRow('标定档位', _getRequestedDisplay()),
const Divider(), const Divider(),
_buildDetailRow('实际功率', _getActualDisplay()), _buildTextRow('实际功率', _getActualDisplay()),
const Divider(), const Divider(),
] else ...[ ] else ...[
_buildDetailRow( _buildTextRow('功率', ChargeUtils.getDisplayPowerWireless(power)),
'功率',
ChargeUtils.getDisplayPowerWireless(power),
),
const Divider(), const Divider(),
], ],
_buildDetailRow('电压', '${volt.toStringAsFixed(2)}V'), _buildDetailRow('电压', volt, 'V'),
const Divider(), const Divider(),
_buildDetailRow('电流', '${curr.toStringAsFixed(2)}A'), _buildDetailRow('电流', curr, 'A'),
const Divider(), const Divider(),
// 辅助信息 if (isWireless) _buildTextRow('类型', '无线'),
if (isWireless) _buildDetailRow('类型', '无线'), if (!isWireless && wiredLevel == null) _buildTextRow('档位', '自动'),
if (!isWireless && wiredLevel == null)
_buildDetailRow('档位', '自动'),
const Spacer(), const Spacer(),
Center( Center(
child: Text( child: Text(
'数据来自 BMS 实时日志', '数据来自 BMS 实时日志',
style: TextStyle(fontSize: 12, color: Colors.grey.shade500), style: TextStyle(
fontFamily: 'HarmonyOS_Sans_SC',
fontSize: 12,
color: Colors.grey.shade500,
),
), ),
), ),
], ],
@@ -107,7 +103,8 @@ class ChargeDetailPage extends StatelessWidget {
); );
} }
Widget _buildDetailRow(String label, String value) { /// 纯文本行(协议、功率、档位等)
Widget _buildTextRow(String label, String value) {
return Padding( return Padding(
padding: const EdgeInsets.symmetric(vertical: 6.0), padding: const EdgeInsets.symmetric(vertical: 6.0),
child: Row( child: Row(
@@ -116,6 +113,7 @@ class ChargeDetailPage extends StatelessWidget {
Text( Text(
label, label,
style: const TextStyle( style: const TextStyle(
fontFamily: 'HarmonyOS_Sans_SC',
fontSize: 16, fontSize: 16,
fontWeight: FontWeight.w400, fontWeight: FontWeight.w400,
color: Colors.grey, color: Colors.grey,
@@ -124,6 +122,7 @@ class ChargeDetailPage extends StatelessWidget {
Text( Text(
value, value,
style: const TextStyle( style: const TextStyle(
fontFamily: 'HarmonyOS_Sans_SC', // 复杂字符串保持 Harmony
fontSize: 16, fontSize: 16,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
color: Colors.black87, color: Colors.black87,
@@ -133,4 +132,44 @@ class ChargeDetailPage extends StatelessWidget {
), ),
); );
} }
/// 数值行(电压/电流,数字用 Flyme,单位用 Harmony
Widget _buildDetailRow(String label, double value, String unit) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 6.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
label,
style: const TextStyle(
fontFamily: 'HarmonyOS_Sans_SC',
fontSize: 16,
fontWeight: FontWeight.w400,
color: Colors.grey,
),
),
RichText(
text: TextSpan(
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: Colors.black87,
),
children: [
TextSpan(
text: value.toStringAsFixed(2),
style: const TextStyle(fontFamily: 'Flyme'), // 🟢 数字用 Flyme
),
TextSpan(
text: unit,
style: const TextStyle(fontFamily: 'HarmonyOS_Sans_SC'),
),
],
),
),
],
),
);
}
} }
+3 -1
View File
@@ -1,6 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
/// 充电入口行(显示充电状态 + 点击进入详情)
class ChargeEntry extends StatelessWidget { class ChargeEntry extends StatelessWidget {
final String summary; final String summary;
final bool isCharging; final bool isCharging;
@@ -23,6 +22,7 @@ class ChargeEntry extends StatelessWidget {
Text( Text(
summary, summary,
style: const TextStyle( style: const TextStyle(
fontFamily: 'HarmonyOS_Sans_SC', // 🟢 混合文本用 Harmony
fontSize: 16, fontSize: 16,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
color: Colors.black87, color: Colors.black87,
@@ -33,6 +33,7 @@ class ChargeEntry extends StatelessWidget {
Text( Text(
'充电信息', '充电信息',
style: TextStyle( style: TextStyle(
fontFamily: 'HarmonyOS_Sans_SC',
fontSize: 16, fontSize: 16,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
color: Colors.grey.shade700, color: Colors.grey.shade700,
@@ -42,6 +43,7 @@ class ChargeEntry extends StatelessWidget {
Text( Text(
'>', '>',
style: TextStyle( style: TextStyle(
fontFamily: 'HarmonyOS_Sans_SC',
fontSize: 16, fontSize: 16,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
color: Colors.grey.shade700, color: Colors.grey.shade700,
+14 -2
View File
@@ -50,7 +50,11 @@ class TrendChart extends StatelessWidget {
children: [ children: [
const Text( const Text(
'容量百分比变动趋势(近15天)', '容量百分比变动趋势(近15天)',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500), style: TextStyle(
fontFamily: 'HarmonyOS_Sans_SC',
fontSize: 16,
fontWeight: FontWeight.w500,
),
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
Row( Row(
@@ -64,6 +68,7 @@ class TrendChart extends StatelessWidget {
Text( Text(
'100', '100',
style: TextStyle( style: TextStyle(
fontFamily: 'Flyme',
fontSize: 10, fontSize: 10,
color: Colors.grey, color: Colors.grey,
height: 1.0, height: 1.0,
@@ -72,6 +77,7 @@ class TrendChart extends StatelessWidget {
Text( Text(
'70', '70',
style: TextStyle( style: TextStyle(
fontFamily: 'Flyme',
fontSize: 10, fontSize: 10,
color: Colors.grey, color: Colors.grey,
height: 1.0, height: 1.0,
@@ -139,6 +145,7 @@ class TrendChart extends StatelessWidget {
child: Text( child: Text(
sevenDaysAgoDate, sevenDaysAgoDate,
style: const TextStyle( style: const TextStyle(
fontFamily: 'HarmonyOS_Sans_SC',
fontSize: 9, fontSize: 9,
color: Colors.grey, color: Colors.grey,
height: 1.0, height: 1.0,
@@ -152,6 +159,7 @@ class TrendChart extends StatelessWidget {
child: Text( child: Text(
todayDate, todayDate,
style: const TextStyle( style: const TextStyle(
fontFamily: 'HarmonyOS_Sans_SC',
fontSize: 9, fontSize: 9,
color: Colors.black, color: Colors.black,
height: 1.0, height: 1.0,
@@ -172,7 +180,11 @@ class TrendChart extends StatelessWidget {
padding: EdgeInsets.only(top: 8.0), padding: EdgeInsets.only(top: 8.0),
child: Text( child: Text(
'暂无历史数据,请点击刷新按钮更新', '暂无历史数据,请点击刷新按钮更新',
style: TextStyle(fontSize: 12, color: Colors.grey), style: TextStyle(
fontFamily: 'HarmonyOS_Sans_SC',
fontSize: 12,
color: Colors.grey,
),
), ),
), ),
], ],
+16
View File
@@ -126,6 +126,14 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "5.0.0" version: "5.0.0"
flutter_markdown:
dependency: "direct main"
description:
name: flutter_markdown
sha256: "08fb8315236099ff8e90cb87bb2b935e0a724a3af1623000a9cec930468e0f27"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.7.7+1"
flutter_svg: flutter_svg:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -224,6 +232,14 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "1.3.0" version: "1.3.0"
markdown:
dependency: transitive
description:
name: markdown
sha256: ee85086ad7698b42522c6ad42fe195f1b9898e4d974a1af4576c1a3a176cada9
url: "https://pub.flutter-io.cn"
source: hosted
version: "7.3.1"
matcher: matcher:
dependency: transitive dependency: transitive
description: description:
+6 -1
View File
@@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts # In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix. # of the product and file versions while build-number is used as the build suffix.
version: 1.5.0 version: 1.6.0
environment: environment:
sdk: ^3.9.2 sdk: ^3.9.2
@@ -38,6 +38,7 @@ dependencies:
path: ^1.9.0 path: ^1.9.0
flutter_svg: ^2.0.10+1 flutter_svg: ^2.0.10+1
shared_preferences: ^2.2.0 shared_preferences: ^2.2.0
flutter_markdown: ^0.7.0
# The following adds the Cupertino Icons font to your application. # The following adds the Cupertino Icons font to your application.
@@ -75,8 +76,12 @@ flutter:
weight: 400 weight: 400
- asset: assets/fonts/HarmonyOS_Sans_SC_Medium.ttf - asset: assets/fonts/HarmonyOS_Sans_SC_Medium.ttf
weight: 500 weight: 500
- family: Flyme
fonts:
- asset: assets/fonts/flymeFont.ttf
assets: assets:
- assets/icons/ - assets/icons/
- assets/CHANGELOG.md
# To add assets to your application, add an assets section, like this: # To add assets to your application, add an assets section, like this:
# assets: # assets: