1 Commits

Author SHA1 Message Date
lxh2875931338 46f8dbc67e log信息来源补充 2026-07-06 19:05:37 +08:00
21 changed files with 339 additions and 1153 deletions
+3
View File
@@ -0,0 +1,3 @@
{
"cmake.ignoreCMakeListsMissing": true
}
+8 -41
View File
@@ -24,50 +24,18 @@
| 版本 | 主题 | 状态 |
| :--- | :--- | :--- |
| 1.5.0 | 功率计算优化 | ✅ 已发布 |
| 1.5.2 | 部分场景字体替换 | ✅ 已发布 |
| 1.6.0 | 更新日志独立页面 | 📋近期开发 |
| 1.7.0 | 适配夜间模式 | 📋近期开发 |
| 2.0.0 | 数据看板月份显示扩展等系列更新 | 📋 远景 |
| 3.0.0 | 搭配PC上位机记录充电信息等 | 📋 远景 |
| 1.4.0 | SAF 权限改造 | ✅ 已发布 |
| 1.4.5 | 魅族20系补充适配调整 | ✅ 已发布 |
| 1.5.0 | 功率计算优化 | 📋 规划中 |
| 2.0.0 | 数据看板扩展(月/季/年趋势) | 📋 远景 |
---
## 环境要求
- Flutter SDK: ^3.9.2
- Android: 8.0+ (API 26+)
---
## 机型匹配情况
| 机型 | 系统版本 | 容量来源 | 设计容量 | 状态 |
|---|---|---|---|---|
| 魅族 22 | Flyme 12.x | 系统未生成 `mbattery_charger` 目录 | — | ❌ 不支持 |
| 魅族 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 12.x | `remaining_cap` + `status=Full` | 4800 mAh | ✅ 1.5.3 字段匹配 |
| 魅族 18 / 18s / 18 Pro / 18s Pro / 18X | Flyme 9 / 10 | 日志存在,字段适配中 | 4000 / 4500 mAh | ⚠️ 等待测试中 |
| 魅族 17 / 17Pro | Flyme 8 / 9 / 10 | 等待群友提供字段信息 | 4500 / 4500mAh | ⚠️ 待验证 |
| 魅族 16th / 16th Plus / 16X / 16s / 16sPro / 16Xs / 16T | Flyme 8 / 9 | 需回退原厂固件验证 | 3600 mAh | ⚠️ 待验证 |
### 📌 说明
-**完美支持**:已通过实机验证,日志解析正常,所有字段完整
- ⚠️ **测试中**:日志存在但字段格式与 20/21 系不同,需要进一步适配
- ⚠️ **待验证**:理论上可能支持,需要实机反馈确认
-**不支持**:系统未生成 BMS 日志目录,或日志格式无法解析
- **系统版本说明**:Flyme 12.x 及以上版本日志格式统一,Flyme 10/11 及以下版本日志格式可能存在差异
目前据群友反馈已知:
- 魅族22无mbattery_charger目录生成,无法实时计算相应百分比,因此已在最近版本去除相关匹配信息
- 魅族18在后续的版本中关闭了mbattery_log生成,疑似无效,老版本可能支持
- 最完美的支持版本至少可覆盖魅族20/21系机型(20inf已确认,机型匹配ID已更新)
- 仅支持魅族手机(15 系列 ~ 22 系列)
---
@@ -78,8 +46,7 @@
flutter pub get
# Debug 版本
flutter run --debug
flutter run
# Release APK
flutter build apk --release --target-platform android-arm64
```
flutter build apk --release
@@ -11,7 +11,6 @@ import android.os.Build
import android.os.Bundle
import android.provider.DocumentsContract
import android.provider.OpenableColumns
import android.view.Window // 🟢 必须包含
import androidx.activity.ComponentActivity
import androidx.annotation.NonNull
import io.flutter.embedding.android.FlutterActivity
@@ -33,9 +32,6 @@ class MainActivity : FlutterActivity() {
private val PREF_NAME = "battery_health_prefs"
private val KEY_SAF_URI = "saf_uri"
// 🟢 刷新率控制通道
private val FRAME_RATE_CHANNEL = "com.lxh.battery_health/frame_rate"
private val chargeReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
intent ?: return
@@ -78,6 +74,7 @@ class MainActivity : FlutterActivity() {
try {
safUri = Uri.parse(uriString)
android.util.Log.d("BatteryHealth", "通过 MethodChannel 恢复 URI: $safUri")
// 同时保存到 SharedPreferences
val prefs: SharedPreferences = getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)
prefs.edit().putString(KEY_SAF_URI, uriString).apply()
} catch (e: Exception) {
@@ -225,34 +222,9 @@ class MainActivity : FlutterActivity() {
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) {
super.configureFlutterEngine(flutterEngine)
// 充电状态通道
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHARGE_CHANNEL)
.setMethodCallHandler { call, result ->
if (call.method == "getChargeState") {
@@ -275,7 +247,6 @@ private fun clearRefreshRate() {
}
}
// SAF 通道
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, SAF_CHANNEL)
.setMethodCallHandler { call, result ->
when (call.method) {
@@ -312,23 +283,6 @@ private fun clearRefreshRate() {
}
}
// 🟢 刷新率控制通道
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)
.setStreamHandler(
object : EventChannel.StreamHandler {
-137
View File
@@ -1,137 +0,0 @@
## [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
@@ -1,3 +0,0 @@
description: This file stores settings for Dart & Flutter DevTools.
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
extensions:
+17 -22
View File
@@ -1,36 +1,31 @@
/// 容量提取工具
/// 支持两种日志格式:
/// 1. learned_cap21 系列):单位 µAh,除以 1000 得到 mAh
/// 🟢 优先级最高,不受 status 限制(BMS 可能在任意状态行更新此值
/// 2. remaining_cap20 系列):仅从 status=Full 行提取,单位智能识别(µAh/mAh)
/// 2. remaining_cap20 系列):单位 mAh,直接使用(但 21 系列的 remaining_cap 也是 µAh,需判断
///
/// 🟢 1.4.5 优化:仅从 status=Full 的行中提取容量(充满状态下 BMS 最准确)
class CapacityExtractor {
static int? extractCapacity(List<String> lines) {
final validLines = lines.where((line) => line.contains('tbat=')).toList();
if (validLines.isEmpty) return null;
// 🟢 只保留包含 tbat= 且 status=Full 的行
final fullLines = lines.where((line) {
return line.contains('tbat=') && line.contains('status=Full');
}).toList();
// ============================================================
// 🟢 第一优先级:learned_cap(21 系列专属)
// 不限制 status,在所有有效行中搜索,取最新的一条
// ============================================================
if (fullLines.isEmpty) {
// 今天没有充满电的记录,返回 null 表示无有效数据
return null;
}
// 模式一:learned_cap21 系列)
final learnedReg = RegExp(r'learned_cap=(\d+)');
for (String line in validLines.reversed) {
for (String line in fullLines.reversed) {
final match = learnedReg.firstMatch(line);
if (match != null) {
final value = int.parse(match.group(1)!);
// learned_cap 单位固定为 µAh,直接除以 1000 转 mAh
return value ~/ 1000;
return int.parse(match.group(1)!) ~/ 1000;
}
}
// ============================================================
// 🟢 第二优先级:remaining_cap20 系列回退方案)
// 仅从 status=Full 行提取(充满状态下 BMS 最准确)
// ============================================================
final fullLines = validLines
.where((line) => line.contains('status=Full'))
.toList();
if (fullLines.isEmpty) return null;
// 模式二:remaining_cap 最大值(20 系列,或 21 系列回退)
final remainingReg = RegExp(r'remaining_cap=(\d+)');
int? maxRemaining;
for (String line in fullLines) {
@@ -44,7 +39,7 @@ class CapacityExtractor {
}
if (maxRemaining == null) return null;
// 🟢 智能单位识别:如果数值大于 100000(约 100 mAh),视为 µAh 需除以 1000
// 🟢 判断单位:如果数值大于 100000(约 100 mAh),则认为是 µAh需除以 1000
if (maxRemaining > 100000) {
return maxRemaining ~/ 1000;
} else {
+2 -3
View File
@@ -38,9 +38,8 @@ class ChargeParser {
final result = <Map<String, dynamic>>[];
for (String line in lines) {
final timestamp = TimeUtils.parseLogTimestamp(line);
if (!TimeUtils.isLogTimestampValid(timestamp, chargingStartTime)) {
if (!TimeUtils.isLogTimestampValid(timestamp, chargingStartTime))
continue;
}
final isUsb =
line.contains('chg_usb') &&
@@ -102,7 +101,7 @@ class ChargeParser {
return ParseResult(
isWireless: false,
chargeType: chargeType,
power: (vbus * ibus) / 1000000000000.0,
power: (vbus * ibus) / 1000000000.0,
volt: vbus / 1000000.0,
curr: ibus / 1000000.0,
wiredLevel: chargeType == 'PPS' ? level : null,
+2 -37
View File
@@ -5,6 +5,7 @@ import 'services/saf_storage_service.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// 🟢 初始化 SAF 服务
await SafStorageService().init();
runApp(const MyApp());
@@ -18,45 +19,9 @@ class MyApp extends StatelessWidget {
return MaterialApp(
title: '电池健康度',
theme: ThemeData(
// 🟢 中文用 HarmonyOS Sans SC
fontFamily: 'HarmonyOS_Sans_SC',
// 🟢 数字回退到 FlymeFont
fontFamilyFallback: const ['Flyme'],
// 🟢 统一文字颜色为黑色
colorScheme: ColorScheme.fromSeed(
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,
),
seedColor: const Color.fromARGB(255, 255, 255, 255),
),
useMaterial3: true,
),
+4 -4
View File
@@ -2,11 +2,11 @@
class DeviceMatching {
/// 根据机型返回设计容量(mAh)
static int getDesignCapacity(String model) {
if (model.contains('22')) return 5510;
if (model.contains('21 Pro')) return 5050;
if (model.contains('21 Note')) return 5500;
if (model.contains('21')) return 4800;
if (model.contains('20 Pro')) return 5000;
if (model.contains('20 Inf')) return 4800;
if (model.contains('20 INFINITY')) return 4800;
if (model.contains('20')) return 4700;
if (model.contains('18s Pro')) return 4500;
if (model.contains('18s')) return 4000;
@@ -29,11 +29,11 @@ class DeviceMatching {
/// 根据机型返回显示名称
static String getModelName(String model) {
if (model.contains('22')) return '魅族22';
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('20 Pro')) return '魅族20 Pro';
if (model.contains('20 Inf')) return '魅族20 Infinity';
if (model.contains('20 INFINITY')) return '魅族20 INFINITY';
if (model.contains('20')) return '魅族20';
if (model.contains('18s Pro')) return '魅族18s Pro';
if (model.contains('18s')) return '魅族18s';
+187 -162
View File
@@ -4,22 +4,21 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:device_info_plus/device_info_plus.dart';
// --- 本地导入 ---
import '../models/device_matching.dart';
import '../models/saf_permission_state.dart';
// 检查这些导入是否正确
import '../models/device_matching.dart'; // ✅ 存在
import '../models/saf_permission_state.dart'; // ✅ 存在
import '../services/database_service.dart';
import '../services/battery_data_service.dart';
import '../services/saf_storage_service.dart';
import '../logic/charge_parser.dart';
import '../logic/capacity_extractor.dart';
import '../utils/time_utils.dart';
import '../utils/charge_utils.dart';
import '../widgets/battery_summary.dart';
import '../widgets/battery_info_panel.dart';
import '../widgets/charge_entry.dart';
import '../widgets/charge_detail_page.dart';
import '../widgets/trend_chart.dart';
import '../pages/changelog_page.dart';
import '../services/saf_storage_service.dart'; // ✅ 存在
import '../logic/charge_parser.dart'; // ✅ 存在
import '../logic/capacity_extractor.dart'; // ✅ 存在(注意:不是 utils/)
import '../utils/time_utils.dart'; // ✅ 存在
import '../utils/charge_utils.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'; // ✅ 存在
// --- 潘通色定义 ---
const Color kHealthGreen = Color(0xFF00B140);
@@ -33,7 +32,8 @@ class BatteryHealthPage extends StatefulWidget {
State<BatteryHealthPage> createState() => _BatteryHealthPageState();
}
class _BatteryHealthPageState extends State<BatteryHealthPage> {
class _BatteryHealthPageState extends State<BatteryHealthPage>
with SingleTickerProviderStateMixin {
// ---------- 数据 ----------
double _currentCap = 0;
int _cycleCount = 0;
@@ -66,6 +66,11 @@ class _BatteryHealthPageState extends State<BatteryHealthPage> {
final BatteryDataService _dataService = BatteryDataService();
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 _eventChannel = EventChannel(
@@ -82,6 +87,8 @@ class _BatteryHealthPageState extends State<BatteryHealthPage> {
super.initState();
dev.log('🔵 ========== initState 开始 ==========', name: 'BatteryHealth');
_initAnimations();
_getInitialChargeState();
_chargeEventSubscription = _eventChannel.receiveBroadcastStream().listen((
@@ -127,10 +134,31 @@ class _BatteryHealthPageState extends State<BatteryHealthPage> {
void dispose() {
_chargeEventSubscription?.cancel();
_pollTimer?.cancel();
_chargeAnimationController.dispose();
_dbService.database.then((db) => db.close()).ignore();
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 {
dev.log('🔵 _getDeviceInfo 开始', name: 'BatteryHealth');
@@ -247,6 +275,7 @@ class _BatteryHealthPageState extends State<BatteryHealthPage> {
_chargeCurr = 0.0;
_wiredLevel = null;
_chargingStartTime = null;
_updateChargeState();
return;
}
@@ -281,6 +310,7 @@ class _BatteryHealthPageState extends State<BatteryHealthPage> {
_chargePower = 0.0;
_chargeVolt = 0.0;
_chargeCurr = 0.0;
_updateChargeState();
});
if (_isCharging) {
@@ -319,6 +349,7 @@ class _BatteryHealthPageState extends State<BatteryHealthPage> {
_currentCap = recent.first['capacity'].toDouble();
_maxVolt = recent.first['volt_max'] ?? 0;
_minVolt = recent.first['volt_min'] ?? 0;
// 🟢 数据库目前不存温度,后续升级可扩展,暂时保留为 0
_maxTemp = 0;
_minTemp = 0;
_cycleCount = 0;
@@ -330,6 +361,8 @@ class _BatteryHealthPageState extends State<BatteryHealthPage> {
setState(() => _hasData = false);
}
if (_isCharging) _fetchChargeDataFromLog();
_chargeAnimationController.reset();
_updateChargeState();
return;
}
@@ -341,16 +374,23 @@ class _BatteryHealthPageState extends State<BatteryHealthPage> {
dev.log('🔵 文件为空', name: 'BatteryHealth');
setState(() => _hasData = false);
if (_isCharging) _fetchChargeDataFromLog();
_chargeAnimationController.reset();
_updateChargeState();
return;
}
// 🟢 提取所有有效行(包含 tbat=)
final validLines = lines.where((line) => line.contains('tbat=')).toList();
if (validLines.isEmpty) {
dev.log('🔵 无有效数据行', name: 'BatteryHealth');
setState(() => _hasData = false);
_chargeAnimationController.reset();
_updateChargeState();
return;
}
// 🟢 1.4.7 核心改动:字段提取与容量提取解耦
// 无论 cap 是否为 null,都提取常规字段
final lastLine = validLines.last;
final cycle = _extractCycle(lastLine);
final temp = _extractTemp(lastLine);
@@ -363,13 +403,16 @@ class _BatteryHealthPageState extends State<BatteryHealthPage> {
name: 'BatteryHealth',
);
// 🟢 容量提取(可能为 null)
final cap = CapacityExtractor.extractCapacity(lines);
int? finalCap;
if (cap != null) {
// ✅ 今日有充满记录,直接使用
finalCap = cap;
dev.log('🔵 从日志提取容量: $cap', name: 'BatteryHealth');
} else {
// ⚠️ 今日无充满记录,尝试从数据库复制前一天容量
dev.log('🔵 今日无充满记录,尝试复制前一天容量', name: 'BatteryHealth');
final recent = await _dbService.queryRecentData(1);
if (recent.isNotEmpty) {
@@ -381,6 +424,7 @@ class _BatteryHealthPageState extends State<BatteryHealthPage> {
}
}
// 🟢 一次性 setState 更新所有字段
setState(() {
_currentCap = (finalCap ?? _currentCap).toDouble();
_cycleCount = cycle;
@@ -399,6 +443,8 @@ class _BatteryHealthPageState extends State<BatteryHealthPage> {
}
await _updateTrendChart();
_chargeAnimationController.reset();
_updateChargeState();
dev.log(
'🔵 ========== readBatteryCapacity 完成 ==========',
name: 'BatteryHealth',
@@ -406,6 +452,8 @@ class _BatteryHealthPageState extends State<BatteryHealthPage> {
} catch (e) {
dev.log('🔵 readBatteryCapacity 异常: $e', name: 'BatteryHealth');
setState(() => _hasData = false);
_chargeAnimationController.reset();
_updateChargeState();
}
}
@@ -539,13 +587,33 @@ 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() {
if (!_isCharging) return '';
if (_isWireless) {
return ChargeUtils.getDisplayPowerWireless(_chargePower);
} else {
return ChargeUtils.getMainDisplayPowerWired(
return ChargeUtils.getDisplayPowerWired(
_chargePower,
_wiredLevel,
_chargeType,
@@ -560,17 +628,9 @@ class _BatteryHealthPageState extends State<BatteryHealthPage> {
// ---------- Build ----------
@override
Widget build(BuildContext context) {
final double healthPercent = (_designCap > 0)
? (_currentCap / _designCap) * 100
: 0.0;
final String chargeSummary = _isCharging
? '充电中 · ${_getChargeTypeLabel()} · ${_getDisplayPower()}'
: '';
return AnnotatedRegion<SystemUiOverlayStyle>(
value: const SystemUiOverlayStyle(
statusBarColor: Color.fromRGBO(157, 212, 237, 0.1),
statusBarColor: Colors.white,
statusBarIconBrightness: Brightness.dark,
statusBarBrightness: Brightness.dark,
),
@@ -597,149 +657,114 @@ class _BatteryHealthPageState extends State<BatteryHealthPage> {
tooltip: '刷新数据',
color: Colors.black,
),
PopupMenuButton<String>(
icon: const Icon(Icons.more_vert),
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;
}
},
itemBuilder: (context) => [
const PopupMenuItem(value: 'changelog', child: Text('更新日志')),
const PopupMenuItem(
value: 'dark_mode',
enabled: false,
child: Text('深色模式(开发中)'),
),
const PopupMenuItem(
value: 'about',
enabled: false,
child: Text('关于(开发中)'),
),
],
),
],
),
body: _hasData
? Column(
body: _hasData ? _buildDataLayout() : _buildEmptyState(),
),
);
}
Widget _buildEmptyState() {
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)),
],
),
);
}
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),
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.toDouble(), // 显式转换
cycleCount: _cycleCount,
currentCap: _currentCap,
designCap: _designCap,
),
const SizedBox(height: 24),
TrendChart(data: _trendData, designCap: _designCap),
const SizedBox(height: 8),
AnimatedBuilder(
animation: _chargeAnimationController,
builder: (context, child) {
final entryHeight = 28 * _entryFadeIn.value;
final chargeEntry = Opacity(
opacity: _entryFadeIn.value,
child: 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,
),
),
);
}
},
),
);
return Column(
children: [
// 🟢 可滚动区域(带 RepaintBoundary
Expanded(
child: SingleChildScrollView(
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 用 RepaintBoundary 隔离
RepaintBoundary(
child: BatterySummary(
healthPercent: healthPercent,
cycleCount: _cycleCount,
currentCap: _currentCap,
designCap: _designCap,
),
),
const SizedBox(height: 24),
// 🟢 TrendChart 用 RepaintBoundary 隔离
RepaintBoundary(
child: TrendChart(
data: _trendData,
designCap: _designCap,
),
),
const SizedBox(height: 8),
// 🟢 充电条目 - 保留 AnimatedOpacity,但加 RepaintBoundary
RepaintBoundary(
child: AnimatedOpacity(
opacity: _isCharging ? 1.0 : 0.0,
duration: const Duration(milliseconds: 300),
child: 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 也用 RepaintBoundary
RepaintBoundary(
child: BatteryInfoPanel(
batteryVolt: _batteryVolt,
batteryTemp: _batteryTemp,
maxVolt: _maxVolt,
minVolt: _minVolt,
maxTemp: _maxTemp,
minTemp: _minTemp,
),
),
],
),
SizedBox(height: entryHeight, child: chargeEntry),
Transform.translate(
offset: Offset(0, _bottomSlide.value),
child: BatteryInfoPanel(
batteryVolt: _batteryVolt,
batteryTemp: _batteryTemp,
maxVolt: _maxVolt,
minVolt: _minVolt,
maxTemp: _maxTemp,
minTemp: _minTemp,
),
),
],
)
: 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),
),
],
),
),
);
},
),
const Spacer(),
],
),
);
}
-205
View File
@@ -1,205 +0,0 @@
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(),
),
);
}
}
+6 -53
View File
@@ -1,7 +1,6 @@
import '../constants/charge_maps.dart';
class ChargeUtils {
// ============ 无线充电(保持不变) ============
static String getDisplayPowerWireless(double power) {
if (power == 0) return '读取中...';
const List<int> levels = [5, 7, 15, 27, 40, 50, 66];
@@ -13,17 +12,21 @@ class ChargeUtils {
return '${matchedLevel}W';
}
// ============ 原有有线逻辑(保留兼容,但主界面不再直接调用) ============
static String getDisplayPowerWired(
double power,
int? wiredLevel,
String chargeType,
) {
// USB 慢充锁定 7.5W
if (chargeType == "SDP") return "≈7.5W";
// 🟢 优先使用 wiredLevel 精确匹配
if (wiredLevel != null && ChargeMaps.wiredPower.containsKey(wiredLevel)) {
double p = ChargeMaps.wiredPower[wiredLevel]!;
if (p > 0) return '${p.toInt()}W';
}
// 🟢 匹配阈值收窄到 ±3W,减少跳变
double best = 0;
for (var entry in ChargeMaps.wiredPower.entries) {
if (entry.value <= power + 3 && entry.value > best) {
@@ -34,61 +37,11 @@ class ChargeUtils {
return best > 0 ? '${best.toInt()}W' : '${power.round()}W';
}
// ============ 🟢 1.5.0 新增:主界面显示(实际/标定) ============
/// 示例:≈30w/65w
static String getMainDisplayPowerWired(
double power,
int? wiredLevel,
String chargeType,
) {
// USB 慢充锁定 7.5W
if (chargeType == "SDP") return "≈7.5w";
if (power == 0) return "读取中...";
final actual = power.round();
// 有档位且能查到标定值
if (wiredLevel != null && ChargeMaps.wiredPower.containsKey(wiredLevel)) {
final requested = ChargeMaps.wiredPower[wiredLevel]!.round();
return "${actual}w/${requested}w";
}
// 无档位信息,仅显示实际功率
return "${actual}w";
}
// ============ 🟢 1.5.0 新增:二级页面标定档位显示 ============
/// 示例:≈80w/L-10
static String getDetailRequestedPowerWired(
double power,
int? wiredLevel,
String chargeType,
) {
if (chargeType == "SDP") return "≈7.5w";
if (power == 0) return "读取中...";
if (wiredLevel != null && ChargeMaps.wiredPower.containsKey(wiredLevel)) {
final requested = ChargeMaps.wiredPower[wiredLevel]!.round();
return "${requested}w/L-$wiredLevel";
}
return "${power.round()}w";
}
// ============ 🟢 1.5.0 新增:二级页面实际功率显示 ============
static String getDetailActualPowerWired(double power, String chargeType) {
if (chargeType == "SDP") return "≈7.5w";
if (power == 0) return "读取中...";
return "${power.round()}w";
}
// ============ 充电类型标签(保持不变) ============
static String getChargeTypeLabel(bool isWireless, String chargeType) {
if (isWireless) {
return chargeType == "无线" ? "无线" : chargeType;
}
// 🟢 空类型显示"识别中...",比"等待log信息"更友好
if (chargeType.isEmpty) {
return "识别中...";
}
-151
View File
@@ -1,151 +0,0 @@
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)),
],
),
);
}
}
+28 -87
View File
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
/// 电池信息面板(电压 + 温度 + 极值)
class BatteryInfoPanel extends StatelessWidget {
final double batteryVolt;
final double batteryTemp;
@@ -36,45 +37,21 @@ class BatteryInfoPanel extends StatelessWidget {
const SizedBox(height: 8),
Row(
children: [
RichText(
text: TextSpan(
style: TextStyle(
fontFamily: 'Flyme',
fontSize: 14,
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'),
],
Text(
'${maxVolt.toStringAsFixed(3)}V',
style: const TextStyle(
color: Colors.red,
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
const SizedBox(width: 12),
RichText(
text: TextSpan(
style: TextStyle(
fontFamily: 'Flyme',
fontSize: 14,
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'),
],
Text(
'${minVolt.toStringAsFixed(3)}V',
style: const TextStyle(
color: Colors.blue,
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
],
@@ -89,50 +66,26 @@ class BatteryInfoPanel extends StatelessWidget {
_buildInfoItem(
_buildIcon('temperature'),
'温度',
'${batteryTemp.toStringAsFixed(0)}',
'${batteryTemp.toStringAsFixed(0)}°C',
),
const SizedBox(height: 8),
Row(
children: [
RichText(
text: TextSpan(
style: TextStyle(
fontFamily: 'Flyme',
fontSize: 14,
fontWeight: FontWeight.w500,
color: Colors.red,
),
children: [
const TextSpan(
text: '',
style: TextStyle(
fontFamily: 'HarmonyOS_Sans_SC',
color: Colors.red,
),
),
TextSpan(text: '${maxTemp.toStringAsFixed(0)}'),
],
Text(
'${maxTemp.toStringAsFixed(0)}°C',
style: const TextStyle(
color: Colors.red,
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
const SizedBox(width: 12),
RichText(
text: TextSpan(
style: TextStyle(
fontFamily: 'Flyme',
fontSize: 14,
fontWeight: FontWeight.w500,
color: Colors.blue,
),
children: [
const TextSpan(
text: '',
style: TextStyle(
fontFamily: 'HarmonyOS_Sans_SC',
color: Colors.blue,
),
),
TextSpan(text: '${minTemp.toStringAsFixed(0)}'),
],
Text(
'${minTemp.toStringAsFixed(0)}°C',
style: const TextStyle(
color: Colors.blue,
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
],
@@ -170,23 +123,11 @@ class BatteryInfoPanel extends StatelessWidget {
children: [
icon,
const SizedBox(width: 6),
Text(
label,
style: const TextStyle(
fontFamily: 'HarmonyOS_Sans_SC',
fontSize: 14,
color: Colors.grey,
),
),
Text(label, style: const TextStyle(fontSize: 14, color: Colors.grey)),
const SizedBox(width: 6),
Text(
value,
style: const TextStyle(
fontFamily: 'Flyme',
fontSize: 16,
fontWeight: FontWeight.w500,
color: Colors.black,
),
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500),
),
],
);
+36 -50
View File
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
/// 电池健康度摘要组件(健康度 + cycles + mAh
class BatterySummary extends StatelessWidget {
final double healthPercent;
final int cycleCount;
@@ -16,8 +17,6 @@ class BatterySummary extends StatelessWidget {
@override
Widget build(BuildContext context) {
final bool isLowHealth = healthPercent < 70;
return Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
@@ -29,22 +28,20 @@ class BatterySummary extends StatelessWidget {
Text(
healthPercent.toStringAsFixed(1),
style: TextStyle(
fontFamily: 'Flyme',
fontSize: 40,
fontWeight: FontWeight.w500,
height: 1.0,
color: isLowHealth ? Colors.red : Colors.black,
color: healthPercent < 70 ? Colors.red : Colors.black,
),
),
const SizedBox(width: 2),
Text(
'%',
style: TextStyle(
fontFamily: 'Flyme',
fontSize: 20,
fontWeight: FontWeight.w300,
height: 1.0,
color: isLowHealth ? Colors.red : Colors.grey.shade600,
color: healthPercent < 70 ? Colors.red : Colors.grey.shade600,
),
),
],
@@ -54,57 +51,46 @@ class BatterySummary extends StatelessWidget {
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
RichText(
text: TextSpan(
style: TextStyle(
fontFamily: 'Flyme',
fontSize: 16,
fontWeight: FontWeight.w500,
height: 1.2,
color: Colors.black,
),
Text(
'$cycleCount cycles', // ✅ 修正:去掉下划线,使用 cycleCount
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
height: 1.2,
),
),
const SizedBox(height: 4),
Text.rich(
TextSpan(
children: [
TextSpan(text: '$cycleCount'),
TextSpan(
text: ' cycles',
text: currentCap.toStringAsFixed(0),
style: const TextStyle(
fontFamily: 'HarmonyOS_Sans_SC',
color: Colors.black,
fontSize: 20,
fontWeight: FontWeight.w300,
),
),
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,
),
),
],
),
),
const SizedBox(height: 4),
RichText(
text: TextSpan(
style: TextStyle(
fontFamily: 'Flyme',
fontSize: 20,
fontWeight: FontWeight.w300,
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),
),
),
],
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.w300,
height: 1.0,
),
),
],
+41 -112
View File
@@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../utils/charge_utils.dart';
/// 充电详情页
class ChargeDetailPage extends StatelessWidget {
final String chargeType;
final bool isWireless;
@@ -20,6 +20,14 @@ class ChargeDetailPage extends StatelessWidget {
this.wiredLevel,
});
String _getDisplayPower() {
if (isWireless) {
return ChargeUtils.getDisplayPowerWireless(power);
} else {
return ChargeUtils.getDisplayPowerWired(power, wiredLevel, chargeType);
}
}
String _getProtocolDisplay() {
if (isWireless) return '无线充电($chargeType';
if (chargeType == 'PPS') return 'PPS 快充';
@@ -27,84 +35,47 @@ class ChargeDetailPage extends StatelessWidget {
return chargeType;
}
String _getRequestedDisplay() {
return ChargeUtils.getDetailRequestedPowerWired(
power,
wiredLevel,
chargeType,
);
}
String _getActualDisplay() {
return ChargeUtils.getDetailActualPowerWired(power, chargeType);
}
@override
Widget build(BuildContext context) {
return AnnotatedRegion<SystemUiOverlayStyle>(
value: const SystemUiOverlayStyle(
statusBarColor: Color.fromRGBO(157, 212, 237, 0.1),
statusBarIconBrightness: Brightness.dark,
statusBarBrightness: Brightness.dark,
return Scaffold(
appBar: AppBar(
title: const Text('充电详情'),
backgroundColor: const Color.fromARGB(0, 255, 255, 255),
elevation: 0,
foregroundColor: Colors.black,
iconTheme: const IconThemeData(color: Colors.black),
),
child: Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
title: const Text('充电详情'),
backgroundColor: Colors.white,
elevation: 0,
scrolledUnderElevation: 0,
shadowColor: Colors.transparent,
surfaceTintColor: Colors.transparent,
foregroundColor: Colors.black,
iconTheme: const IconThemeData(color: Colors.black),
),
body: Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildTextRow('协议', _getProtocolDisplay()),
const Divider(),
if (!isWireless) ...[
_buildTextRow('标定档位', _getRequestedDisplay()),
const Divider(),
_buildTextRow('实际功率', _getActualDisplay()),
const Divider(),
] else ...[
_buildTextRow('功率', ChargeUtils.getDisplayPowerWireless(power)),
const Divider(),
],
_buildDetailRow('电压', volt, 'V'),
const Divider(),
_buildDetailRow('电流', curr, 'A'),
const Divider(),
if (isWireless) _buildTextRow('类型', '无线'),
if (!isWireless && wiredLevel == null) _buildTextRow('档位', '自动'),
const Spacer(),
Center(
child: Text(
'数据来自 BMS 实时日志',
style: TextStyle(
fontFamily: 'HarmonyOS_Sans_SC',
fontSize: 12,
color: Colors.grey.shade500,
),
),
body: Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildDetailRow('协议', _getProtocolDisplay()),
const Divider(),
_buildDetailRow('功率', _getDisplayPower()),
const Divider(),
_buildDetailRow('电压', '${volt.toStringAsFixed(2)}V'),
const Divider(),
_buildDetailRow('电流', '${curr.toStringAsFixed(2)}A'),
const Divider(),
if (!isWireless && wiredLevel != null)
_buildDetailRow('档位', '$wiredLevel'),
if (!isWireless && wiredLevel == null) _buildDetailRow('档位', '自动'),
if (isWireless) _buildDetailRow('类型', '无线'),
const Spacer(),
Center(
child: Text(
'数据来自 BMS 实时日志',
style: TextStyle(fontSize: 12, color: Colors.grey.shade500),
),
],
),
),
],
),
),
);
}
/// 纯文本行(协议、功率、档位等)
Widget _buildTextRow(String label, String value) {
Widget _buildDetailRow(String label, String value) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 6.0),
child: Row(
@@ -113,7 +84,6 @@ class ChargeDetailPage extends StatelessWidget {
Text(
label,
style: const TextStyle(
fontFamily: 'HarmonyOS_Sans_SC',
fontSize: 16,
fontWeight: FontWeight.w400,
color: Colors.grey,
@@ -122,7 +92,6 @@ class ChargeDetailPage extends StatelessWidget {
Text(
value,
style: const TextStyle(
fontFamily: 'HarmonyOS_Sans_SC', // 复杂字符串保持 Harmony
fontSize: 16,
fontWeight: FontWeight.w500,
color: Colors.black87,
@@ -132,44 +101,4 @@ 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'),
),
],
),
),
],
),
);
}
}
+1 -3
View File
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
/// 充电入口行(显示充电状态 + 点击进入详情)
class ChargeEntry extends StatelessWidget {
final String summary;
final bool isCharging;
@@ -22,7 +23,6 @@ class ChargeEntry extends StatelessWidget {
Text(
summary,
style: const TextStyle(
fontFamily: 'HarmonyOS_Sans_SC', // 🟢 混合文本用 Harmony
fontSize: 16,
fontWeight: FontWeight.w500,
color: Colors.black87,
@@ -33,7 +33,6 @@ class ChargeEntry extends StatelessWidget {
Text(
'充电信息',
style: TextStyle(
fontFamily: 'HarmonyOS_Sans_SC',
fontSize: 16,
fontWeight: FontWeight.w500,
color: Colors.grey.shade700,
@@ -43,7 +42,6 @@ class ChargeEntry extends StatelessWidget {
Text(
'>',
style: TextStyle(
fontFamily: 'HarmonyOS_Sans_SC',
fontSize: 16,
fontWeight: FontWeight.w500,
color: Colors.grey.shade700,
+2 -14
View File
@@ -50,11 +50,7 @@ class TrendChart extends StatelessWidget {
children: [
const Text(
'容量百分比变动趋势(近15天)',
style: TextStyle(
fontFamily: 'HarmonyOS_Sans_SC',
fontSize: 16,
fontWeight: FontWeight.w500,
),
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500),
),
const SizedBox(height: 8),
Row(
@@ -68,7 +64,6 @@ class TrendChart extends StatelessWidget {
Text(
'100',
style: TextStyle(
fontFamily: 'Flyme',
fontSize: 10,
color: Colors.grey,
height: 1.0,
@@ -77,7 +72,6 @@ class TrendChart extends StatelessWidget {
Text(
'70',
style: TextStyle(
fontFamily: 'Flyme',
fontSize: 10,
color: Colors.grey,
height: 1.0,
@@ -145,7 +139,6 @@ class TrendChart extends StatelessWidget {
child: Text(
sevenDaysAgoDate,
style: const TextStyle(
fontFamily: 'HarmonyOS_Sans_SC',
fontSize: 9,
color: Colors.grey,
height: 1.0,
@@ -159,7 +152,6 @@ class TrendChart extends StatelessWidget {
child: Text(
todayDate,
style: const TextStyle(
fontFamily: 'HarmonyOS_Sans_SC',
fontSize: 9,
color: Colors.black,
height: 1.0,
@@ -180,11 +172,7 @@ class TrendChart extends StatelessWidget {
padding: EdgeInsets.only(top: 8.0),
child: Text(
'暂无历史数据,请点击刷新按钮更新',
style: TextStyle(
fontFamily: 'HarmonyOS_Sans_SC',
fontSize: 12,
color: Colors.grey,
),
style: TextStyle(fontSize: 12, color: Colors.grey),
),
),
],
-16
View File
@@ -126,14 +126,6 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
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:
dependency: "direct main"
description:
@@ -232,14 +224,6 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
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:
dependency: transitive
description:
+1 -6
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
# 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.
version: 1.6.0
version: 1.4.7
environment:
sdk: ^3.9.2
@@ -38,7 +38,6 @@ dependencies:
path: ^1.9.0
flutter_svg: ^2.0.10+1
shared_preferences: ^2.2.0
flutter_markdown: ^0.7.0
# The following adds the Cupertino Icons font to your application.
@@ -76,12 +75,8 @@ flutter:
weight: 400
- asset: assets/fonts/HarmonyOS_Sans_SC_Medium.ttf
weight: 500
- family: Flyme
fonts:
- asset: assets/fonts/flymeFont.ttf
assets:
- assets/icons/
- assets/CHANGELOG.md
# To add assets to your application, add an assets section, like this:
# assets: