6 Commits
20 changed files with 993 additions and 335 deletions
+79
View File
@@ -0,0 +1,79 @@
# ============ Flutter 核心 ============
-keep class io.flutter.** { *; }
-keep class io.flutter.plugins.** { *; }
-keep class io.flutter.plugin.** { *; }
-keep class io.flutter.util.** { *; }
-keep class io.flutter.view.** { *; }
-keep class io.flutter.embedding.** { *; }
# 保留 Flutter 的 JNI 调用类
-keep class io.flutter.embedding.engine.FlutterJNI { *; }
-dontwarn io.flutter.embedding.**
# ============ Kotlin ============
-keep class kotlin.** { *; }
-keep class kotlin.Metadata { *; }
-dontwarn kotlin.**
-keepclassmembers class **$WhenMappings {
<fields>;
}
-keepclassmembers class kotlin.Metadata {
public <methods>;
}
# ============ sqflite / SQLite ============
-keep class com.tekartik.sqflite.** { *; }
-dontwarn com.tekartik.sqflite.**
# ============ shared_preferences ============
-keep class io.flutter.plugins.sharedpreferences.** { *; }
# ============ path_provider ============
-keep class io.flutter.plugins.pathprovider.** { *; }
# ============ permission_handler ============
-keep class com.baseflow.permissionhandler.** { *; }
# ============ url_launcher ============
-keep class io.flutter.plugins.urllauncher.** { *; }
# ============ device_info_plus ============
-keep class dev.fluttercommunity.plus.device_info.** { *; }
# ============ 你的原生通道(重要!) ============
# 你的 MethodChannel / EventChannel 相关类需要保留
-keep class com.lxh.battery_health.** { *; }
# ============ AndroidX 常见 ============
-keep class androidx.lifecycle.** { *; }
-dontwarn androidx.lifecycle.**
# ============ 保留 Parcelable / Serializable ============
-keepclassmembers class * implements android.os.Parcelable {
public static final android.os.Parcelable$Creator *;
}
-keepclassmembers class * implements java.io.Serializable {
static final long serialVersionUID;
private static final java.io.ObjectStreamField[] serialPersistentFields;
private void writeObject(java.io.ObjectOutputStream);
private void readObject(java.io.ObjectInputStream);
java.lang.Object writeReplace();
java.lang.Object readResolve();
}
# ============ 异常堆栈保留(方便排查 release 崩溃) ============
-keepattributes SourceFile,LineNumberTable
-renamesourcefileattribute SourceFile
-keepattributes Signature
-keepattributes *Annotation*
-keepattributes Exceptions
-keepattributes InnerClasses
-keepattributes EnclosingMethod
# ============ 移除日志(可选) ============
# 如果你想在 release 里保留日志,注释掉下面这几行
-assumenosideeffects class android.util.Log {
public static *** v(...);
public static *** d(...);
public static *** i(...);
}
@@ -0,0 +1,51 @@
package com.lxh.battery_health
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.BatteryManager
class BatteryManagerHelper(private val context: Context) {
fun getBatteryData(): Map<String, Any?> {
val bm = context.getSystemService(Context.BATTERY_SERVICE) as BatteryManager
// 电流(µA)。部分机型充电为负、放电为正,部分相反
val rawCurrentUa = bm.getIntProperty(BatteryManager.BATTERY_PROPERTY_CURRENT_NOW)
// 电压 / 状态从 ACTION_BATTERY_CHANGED 广播里拿(比 BATTERY_PROPERTY_VOLTAGE 兼容性好)
val intent = context.registerReceiver(
null,
IntentFilter(Intent.ACTION_BATTERY_CHANGED)
)
val voltageMv = intent?.getIntExtra(BatteryManager.EXTRA_VOLTAGE, -1) ?: -1
val status = intent?.getIntExtra(BatteryManager.EXTRA_STATUS, -1) ?: -1
val plugged = intent?.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1) ?: -1
val temperatureTenth = intent?.getIntExtra(BatteryManager.EXTRA_TEMPERATURE, -1) ?: -1
val isCharging = status == BatteryManager.BATTERY_STATUS_CHARGING ||
status == BatteryManager.BATTERY_STATUS_FULL
// 符号规约:充电为正
val normalizedUa = when {
rawCurrentUa == Int.MIN_VALUE -> 0
isCharging && rawCurrentUa < 0 -> -rawCurrentUa
!isCharging && rawCurrentUa > 0 -> -rawCurrentUa
else -> rawCurrentUa
}
// 功率 W = V × A = (mV / 1000) × (µA / 1_000_000)
val powerW = if (voltageMv > 0 && normalizedUa != 0) {
(voltageMv / 1000.0) * (normalizedUa / 1_000_000.0)
} else 0.0
return mapOf(
"voltageMv" to voltageMv,
"currentUa" to normalizedUa,
"powerW" to powerW,
"temperatureC" to if (temperatureTenth > 0) temperatureTenth / 10.0 else 0.0,
"plugged" to plugged, // 0=未插, 1=AC, 2=USB, 4=WIRELESS
"isCharging" to isCharging,
)
}
}
@@ -255,26 +255,35 @@ private fun clearRefreshRate() {
// 充电状态通道
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHARGE_CHANNEL)
.setMethodCallHandler { call, result ->
if (call.method == "getChargeState") {
val isCharging = getBatteryStatus()
val chargeType = if (isCharging) {
val batteryIntent = registerReceiver(null, IntentFilter(Intent.ACTION_BATTERY_CHANGED))
val plug = batteryIntent?.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1) ?: -1
when (plug) {
BatteryManager.BATTERY_PLUGGED_AC -> "AC"
BatteryManager.BATTERY_PLUGGED_USB -> "USB"
BatteryManager.BATTERY_PLUGGED_WIRELESS -> "WIRELESS"
else -> "UNKNOWN"
}
} else {
"NONE"
.setMethodCallHandler { call, result ->
when (call.method) {
"getChargeState" -> {
val isCharging = getBatteryStatus()
val chargeType = if (isCharging) {
val batteryIntent = registerReceiver(null, IntentFilter(Intent.ACTION_BATTERY_CHANGED))
val plug = batteryIntent?.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1) ?: -1
when (plug) {
BatteryManager.BATTERY_PLUGGED_AC -> "AC"
BatteryManager.BATTERY_PLUGGED_USB -> "USB"
BatteryManager.BATTERY_PLUGGED_WIRELESS -> "WIRELESS"
else -> "UNKNOWN"
}
result.success(mapOf("isCharging" to isCharging, "chargeType" to chargeType))
} else {
result.notImplemented()
"NONE"
}
result.success(mapOf("isCharging" to isCharging, "chargeType" to chargeType))
}
// 🆕 新增分支
"getBatteryManagerData" -> {
try {
result.success(BatteryManagerHelper(applicationContext).getBatteryData())
} catch (e: Exception) {
result.error("BM_ERROR", e.message, null)
}
}
else -> result.notImplemented()
}
}
// SAF 通道
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, SAF_CHANNEL)
File diff suppressed because one or more lines are too long
+67
View File
@@ -1,3 +1,70 @@
## [1.9.0] - 2026-10-11
### ✨ 新增功能
- **BatteryManager 实时数据接入**
- 新增原生 `BatteryManagerHelper`,通过 Android 官方 API 读取电池端真实功率
- 主界面充电行改为显示电池端实时功率(1 秒刷新),反映电池实际吸收的能量
- 充电详情页拆分为两个数据源分区:
- **BMS 日志**:协议、标定档位、实际功率、电压、电流(原有)
- **BatteryManager(电池端)**:电池端电压、电流、功率(新增)
- 与专业功率计(PowerZ)实测数据一致,误差在 0.02W 以内
### 🎨 体验优化
- **充电行显示格式调整**
- 有线充电:`充电中 · PPS · 5.98W / 7.5W`(实际功率 / 标定档位)
- 无线充电:`充电中 · 无线 · 12.3W`
- USB 慢充:`充电中 · USB · 2.1W`
- 瓦特单位统一为大写 `W`
- 7.5W 等带小数的档位保留一位小数,其余整数值去掉 `.0`
- **字体细节优化**
- 充电行数字与拉丁字母改用 Flyme 字体,中文字符保持 HarmonyOS Sans SC
- 解决混合文本下的字体割裂感
- 充电详情页所有功率数值同步应用 Flyme 字体
### 🐛 问题修复
- 修复主界面充电行在充电器接入初期显示"读取中..."后数值迟迟不刷新的问题
- 修复详情页功率数值字体与其它行不统一的问题
- 修复 `wired_level` 解析后标定档位显示取整误差(如 `≈8W/L-1` 修正为 `≈7.5W/L-1`)
### 📝 技术说明
- `BATTERY_PROPERTY_CURRENT_NOW` 读数为**电池端净电流**(充电器输入减去系统功耗),而非充电器输出端数据
- 电池接近满电时数值会明显偏低(涓流阶段),属正常现象
- 与系统状态栏显示的充电功率为不同概念,前者反映电池实际吸收,后者反映充电器输出
---
## [1.8.0] - 2026-10-11
### 新增
- **夜间模式**:支持浅色 / 深色 / 跟随系统三种模式,可在右上角菜单切换
- **主题系统重构**:统一颜色抽象,所有界面颜色由主题管理
### 优化
- 深色主题下潘通色系重新调色,保证对比度
- AppBar 与内容区背景统一,消除浅色模式下的分界线
----
## [1.7.1] - 2026-09-16
### 新增
- **平均温度显示**:温度面板新增平均温度,与最高/最低温度同排展示(`↑ xx.x℃ ↓ xx.x℃ ⌀ xx.x℃`),更直观了解电池工作温度区间
### 优化
- **数据库升级至 v2**:新增 `temp_avg` 字段,历史数据自动迁移,每日平均温度随扫描自动入库
- **构建工具链同步**:适配最新 Flutter SDK 的 Android 构建要求(AGP 9.0 / Kotlin 2.2 / Gradle 9.3)
- **Release 打包加固**:补齐 ProGuard 混淆规则,release 版本构建更稳定
### 修复
- 修复 Release 版本因缺少混淆规则文件导致打包失败的问题
----
## [1.7.0] - 2026-07-31
### ✨ 新增功能
+65 -45
View File
@@ -1,11 +1,14 @@
import 'package:flutter/material.dart';
import 'pages/battery_health_page.dart';
import 'services/saf_storage_service.dart';
import 'theme/app_colors.dart';
import 'theme/theme_service.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await SafStorageService().init();
await ThemeService.init(); // 🆕
runApp(const MyApp());
}
@@ -15,53 +18,70 @@ class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
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,
),
return ValueListenableBuilder<ThemeMode>(
valueListenable: ThemeService.notifier,
builder: (context, mode, _) {
return MaterialApp(
title: '电池健康度',
themeMode: mode,
theme: _buildTheme(Brightness.light, kLightColors),
darkTheme: _buildTheme(Brightness.dark, kDarkColors),
home: const BatteryHealthPage(),
debugShowCheckedModeBanner: false,
);
},
);
}
ThemeData _buildTheme(Brightness brightness, AppColors colors) {
return ThemeData(
useMaterial3: true,
brightness: brightness,
scaffoldBackgroundColor: colors.background,
fontFamily: 'HarmonyOS_Sans_SC',
fontFamilyFallback: const ['Flyme'],
colorScheme: ColorScheme.fromSeed(
seedColor: colors.background,
brightness: brightness,
),
extensions: [colors], // 🆕 注册 ThemeExtension
appBarTheme: AppBarTheme(
backgroundColor: colors.background, // ← 关键:和 Scaffold 一致
foregroundColor: colors.textPrimary,
// ← 关掉 M3 的滑动变色
elevation: 0,
iconTheme: IconThemeData(color: colors.textPrimary),
),
textTheme: TextTheme(
displayMedium: TextStyle(
fontFamily: 'HarmonyOS_Sans_SC',
fontFamilyFallback: const ['Flyme'],
color: colors.textPrimary,
),
useMaterial3: true,
headlineMedium: TextStyle(
fontFamily: 'HarmonyOS_Sans_SC',
fontFamilyFallback: const ['Flyme'],
color: colors.textPrimary,
),
bodyMedium: TextStyle(
fontFamily: 'HarmonyOS_Sans_SC',
fontFamilyFallback: const ['Flyme'],
color: colors.textPrimary,
),
bodySmall: TextStyle(
fontFamily: 'HarmonyOS_Sans_SC',
fontFamilyFallback: const ['Flyme'],
color: colors.textSecondary,
),
displayLarge: TextStyle(
fontFamily: 'HarmonyOS_Sans_SC',
fontFamilyFallback: const ['Flyme'],
color: colors.textPrimary,
),
),
home: const BatteryHealthPage(),
debugShowCheckedModeBanner: false,
);
}
}
+19 -37
View File
@@ -1,16 +1,16 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:url_launcher/url_launcher.dart';
import '../theme/app_colors.dart';
class AboutPage extends StatelessWidget {
const AboutPage({super.key});
@override
Widget build(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
final c = context.colors;
return Scaffold(
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
appBar: AppBar(
title: const Text(
'关于',
@@ -19,10 +19,8 @@ class AboutPage extends StatelessWidget {
fontWeight: FontWeight.w500,
),
),
backgroundColor: Colors.transparent,
backgroundColor: c.background,
elevation: 0,
foregroundColor: Theme.of(context).iconTheme.color,
iconTheme: IconThemeData(color: Theme.of(context).iconTheme.color),
),
body: Center(
child: Padding(
@@ -30,15 +28,12 @@ class AboutPage extends StatelessWidget {
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Logo
Container(
width: 120,
height: 120,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: isDark
? Colors.grey.shade800
: const Color(0xFFF0F4F8),
color: c.surface,
),
child: ClipOval(
child: Image.asset(
@@ -50,38 +45,39 @@ class AboutPage extends StatelessWidget {
),
),
const SizedBox(height: 24),
const Text(
Text(
'MEIZU Battery Healthy',
style: TextStyle(
fontFamily: 'HarmonyOS_Sans_SC',
fontSize: 24,
fontWeight: FontWeight.w600,
color: c.textPrimary,
),
),
const SizedBox(height: 8),
Text(
'版本 1.7.0',
'版本 1.8.0',
style: TextStyle(
fontFamily: 'HarmonyOS_Sans_SC',
fontSize: 14,
color: Colors.grey.shade500,
color: c.textTertiary,
fontWeight: FontWeight.w400,
),
),
const SizedBox(height: 16),
Divider(
color: Colors.grey.shade300,
color: c.divider,
thickness: 0.5,
indent: 40,
endIndent: 40,
),
const SizedBox(height: 16),
const Text(
Text(
'专机专配 · 仅针对魅族独家开发',
style: TextStyle(
fontFamily: 'HarmonyOS_Sans_SC',
fontSize: 14,
color: Colors.grey,
color: c.textSecondary,
),
),
const SizedBox(height: 8),
@@ -90,13 +86,12 @@ class AboutPage extends StatelessWidget {
style: TextStyle(
fontFamily: 'HarmonyOS_Sans_SC',
fontSize: 13,
color: Colors.grey.shade400,
color: c.textTertiary,
),
),
const SizedBox(height: 24),
// 源码链接按钮
InkWell(
onTap: () => _launchUrl(context), // 注意这里传入了 context
onTap: () => _launchUrl(context),
borderRadius: BorderRadius.circular(12),
child: Container(
padding: const EdgeInsets.symmetric(
@@ -104,34 +99,24 @@ class AboutPage extends StatelessWidget {
vertical: 12,
),
decoration: BoxDecoration(
color: isDark
? Colors.grey.shade800
: const Color(0xFFF0F4F8),
color: c.surface,
borderRadius: BorderRadius.circular(12),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.code,
size: 18,
color: Theme.of(context).iconTheme.color,
),
Icon(Icons.code, size: 18, color: c.textPrimary),
const SizedBox(width: 8),
Text(
'查看源码 / 反馈',
style: TextStyle(
fontFamily: 'HarmonyOS_Sans_SC',
fontSize: 14,
color: Theme.of(context).iconTheme.color,
color: c.textPrimary,
),
),
const SizedBox(width: 4),
Icon(
Icons.open_in_new,
size: 14,
color: Colors.grey.shade500,
),
Icon(Icons.open_in_new, size: 14, color: c.textTertiary),
],
),
),
@@ -142,7 +127,7 @@ class AboutPage extends StatelessWidget {
style: TextStyle(
fontFamily: 'HarmonyOS_Sans_SC',
fontSize: 12,
color: Colors.grey.shade400,
color: c.textTertiary,
),
),
],
@@ -155,14 +140,11 @@ class AboutPage extends StatelessWidget {
Future<void> _launchUrl(BuildContext context) async {
const url = 'https://git.whitetop.xyz/lxh2875931338/MEIZU-Battery-Healthy';
final uri = Uri.parse(url);
try {
// ✅ 直接尝试打开,不再检查 canLaunchUrl
await launchUrl(uri, mode: LaunchMode.externalApplication);
} catch (e) {
// 如果打开失败,复制链接并提示
debugPrint('打开链接失败: $e');
await Clipboard.setData(ClipboardData(text: url));
await Clipboard.setData(const ClipboardData(text: url));
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
+118 -34
View File
@@ -3,6 +3,9 @@ import 'dart:developer' as dev;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:device_info_plus/device_info_plus.dart';
import '../theme/theme_service.dart';
import '../theme/app_colors.dart';
import '../services/battery_manager_service.dart';
// --- 本地导入 ---
import '../models/device_matching.dart';
@@ -62,6 +65,9 @@ class _BatteryHealthPageState extends State<BatteryHealthPage> {
int? _wiredLevel;
bool _isWireless = false;
DateTime? _chargingStartTime;
double _bmPower = 0.0;
double _bmVolt = 0.0;
double _bmCurr = 0.0;
// ---------- SAF 状态 ----------
final SafStorageService _safService = SafStorageService();
@@ -78,6 +84,7 @@ class _BatteryHealthPageState extends State<BatteryHealthPage> {
// 轮询定时器(15秒)
Timer? _pollTimer;
Timer? _bmTimer;
// ---------- 生命周期 ----------
@override
@@ -122,7 +129,7 @@ class _BatteryHealthPageState extends State<BatteryHealthPage> {
readBatteryCapacity();
}
});
_startBmPolling(); // ← 调用,不是定义
dev.log('🔵 ========== initState 结束 ==========', name: 'BatteryHealth');
}
@@ -130,6 +137,7 @@ class _BatteryHealthPageState extends State<BatteryHealthPage> {
void dispose() {
_chargeEventSubscription?.cancel();
_pollTimer?.cancel();
_bmTimer?.cancel();
_dbService.database.then((db) => db.close()).ignore();
super.dispose();
}
@@ -250,6 +258,9 @@ class _BatteryHealthPageState extends State<BatteryHealthPage> {
_chargeCurr = 0.0;
_wiredLevel = null;
_chargingStartTime = null;
_bmPower = 0.0; // 🆕
_bmVolt = 0.0; // 🆕
_bmCurr = 0.0; // 🆕
return;
}
@@ -608,24 +619,90 @@ class _BatteryHealthPageState extends State<BatteryHealthPage> {
}
}
// ---------- BM 轮询(1秒) ----------
void _startBmPolling() {
_bmTimer?.cancel();
_bmTimer = Timer.periodic(const Duration(seconds: 1), (_) async {
if (!_isCharging) return;
try {
final d = await BatteryManagerService.fetch();
if (!mounted) return;
setState(() {
_bmPower = d.powerW;
_bmVolt = d.voltageV;
_bmCurr = d.currentA;
});
} catch (e) {
// 静默降级,保留上一次的值
}
});
}
// ---------- 显示辅助 ----------
String _getDisplayPower() {
if (!_isCharging) return '';
if (_isWireless) {
return ChargeUtils.getDisplayPowerWireless(_chargePower);
} else {
return ChargeUtils.getMainDisplayPowerWired(
_chargePower,
_wiredLevel,
_chargeType,
);
}
return ChargeUtils.getMainDisplayPowerBm(
_bmPower,
_wiredLevel,
_chargeType,
_isWireless,
);
}
String _getChargeTypeLabel() {
return ChargeUtils.getChargeTypeLabel(_isWireless, _chargeType);
}
// 🆕 夜间模式切换弹窗 ← 加在这里
void _showThemePicker() {
showModalBottomSheet(
context: context,
builder: (ctx) {
final current = ThemeService.notifier.value;
return SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
leading: const Icon(Icons.brightness_auto),
title: const Text('跟随系统'),
trailing: current == ThemeMode.system
? const Icon(Icons.check)
: null,
onTap: () {
ThemeService.setMode(ThemeMode.system);
Navigator.pop(ctx);
},
),
ListTile(
leading: const Icon(Icons.light_mode),
title: const Text('浅色'),
trailing: current == ThemeMode.light
? const Icon(Icons.check)
: null,
onTap: () {
ThemeService.setMode(ThemeMode.light);
Navigator.pop(ctx);
},
),
ListTile(
leading: const Icon(Icons.dark_mode),
title: const Text('深色'),
trailing: current == ThemeMode.dark
? const Icon(Icons.check)
: null,
onTap: () {
ThemeService.setMode(ThemeMode.dark);
Navigator.pop(ctx);
},
),
],
),
);
},
);
}
// ---------- Build ----------
@override
Widget build(BuildContext context) {
@@ -645,25 +722,23 @@ class _BatteryHealthPageState extends State<BatteryHealthPage> {
);
return AnnotatedRegion<SystemUiOverlayStyle>(
value: const SystemUiOverlayStyle(
statusBarColor: Color.fromRGBO(157, 212, 237, 0.1),
value: SystemUiOverlayStyle(
statusBarColor: context.colors.statusBarTint,
statusBarIconBrightness: Brightness.dark,
statusBarBrightness: Brightness.dark,
),
child: Scaffold(
backgroundColor: Colors.white,
// backgroundColor: Colors.white,
appBar: AppBar(
backgroundColor: Colors.white,
// backgroundColor: Colors.white,
elevation: 0,
scrolledUnderElevation: 0,
shadowColor: Colors.transparent,
surfaceTintColor: Colors.transparent,
title: const Text(
title: Text(
'电池健康度',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.w600,
color: Colors.black,
color: context.colors.textPrimary,
),
),
actions: [
@@ -671,11 +746,11 @@ class _BatteryHealthPageState extends State<BatteryHealthPage> {
icon: const Icon(Icons.refresh),
onPressed: readBatteryCapacity,
tooltip: '刷新数据',
color: Colors.black,
color: context.colors.icon,
),
PopupMenuButton<String>(
icon: const Icon(Icons.more_vert),
color: Colors.white,
color: context.colors.surface,
onSelected: (value) {
switch (value) {
case 'changelog':
@@ -695,7 +770,7 @@ class _BatteryHealthPageState extends State<BatteryHealthPage> {
);
break;
case 'dark_mode':
// 夜间模式切换逻辑(1.7.0 再实现)
_showThemePicker();
break;
}
},
@@ -705,11 +780,7 @@ class _BatteryHealthPageState extends State<BatteryHealthPage> {
value: 'about', // 🟢 新增
child: Text('关于'),
),
const PopupMenuItem(
value: 'dark_mode',
enabled: false, // 暂时灰显
child: Text('夜间模式(开发中)'),
),
const PopupMenuItem(value: 'dark_mode', child: Text('夜间模式')),
],
),
],
@@ -725,9 +796,9 @@ class _BatteryHealthPageState extends State<BatteryHealthPage> {
children: [
Text(
_deviceModel,
style: const TextStyle(
style: TextStyle(
fontSize: 14,
color: Colors.grey,
color: context.colors.textTertiary,
fontWeight: FontWeight.w400,
),
),
@@ -774,6 +845,9 @@ class _BatteryHealthPageState extends State<BatteryHealthPage> {
volt: _chargeVolt,
curr: _chargeCurr,
wiredLevel: _wiredLevel,
bmPower: _bmPower, // 🆕
bmVolt: _bmVolt, // 🆕
bmCurr: _bmCurr, // 🆕
),
),
);
@@ -804,19 +878,29 @@ class _BatteryHealthPageState extends State<BatteryHealthPage> {
),
],
)
: const Center(
: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.battery_unknown, size: 64, color: Colors.grey),
SizedBox(height: 16),
Icon(
Icons.battery_unknown,
size: 64,
color: context.colors.textTertiary,
),
const SizedBox(height: 16),
Text(
'暂无数据',
style: TextStyle(fontSize: 18, color: Colors.grey),
style: TextStyle(
fontSize: 18,
color: context.colors.textTertiary,
),
),
Text(
'点击右上角刷新尝试',
style: TextStyle(fontSize: 14, color: Colors.grey),
style: TextStyle(
fontSize: 14,
color: context.colors.textTertiary,
),
),
],
),
+27 -40
View File
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; // 🟢 添加这一行
import 'package:flutter/services.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import '../theme/app_colors.dart';
class ChangelogPage extends StatefulWidget {
const ChangelogPage({super.key});
@@ -15,33 +16,27 @@ class _ChangelogPageState extends State<ChangelogPage>
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),
@@ -70,16 +65,13 @@ class _ChangelogPageState extends State<ChangelogPage>
@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;
@@ -96,7 +88,6 @@ class _ChangelogPageState extends State<ChangelogPage>
setState(() {
_markdownContent = content;
_isLoading = false;
_cachedMarkdown = _buildMarkdown(content);
});
} catch (e) {
setState(() {
@@ -106,78 +97,74 @@ class _ChangelogPageState extends State<ChangelogPage>
}
}
Widget _buildMarkdown(String content) {
Widget _buildMarkdown(BuildContext context, String content) {
final c = context.colors;
return Markdown(
data: content,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
styleSheet: MarkdownStyleSheet(
h1: const TextStyle(
h1: TextStyle(
fontFamily: 'HarmonyOS_Sans_SC',
fontSize: 24,
fontWeight: FontWeight.w700,
color: Colors.black,
color: c.textPrimary,
),
h2: const TextStyle(
h2: TextStyle(
fontFamily: 'HarmonyOS_Sans_SC',
fontSize: 20,
fontWeight: FontWeight.w600,
color: Colors.black87,
color: c.textPrimary,
),
h3: const TextStyle(
h3: TextStyle(
fontFamily: 'HarmonyOS_Sans_SC',
fontSize: 18,
fontWeight: FontWeight.w600,
color: Colors.black87,
color: c.textSecondary,
),
p: const TextStyle(
p: TextStyle(
fontFamily: 'HarmonyOS_Sans_SC',
fontSize: 16,
color: Colors.black87,
color: c.textSecondary,
),
listBullet: const TextStyle(
listBullet: TextStyle(
fontFamily: 'HarmonyOS_Sans_SC',
fontSize: 16,
color: Colors.black87,
color: c.textSecondary,
),
code: const TextStyle(
code: TextStyle(
fontFamily: 'HarmonyOS_Sans_SC',
fontSize: 14,
color: Colors.black87,
backgroundColor: Color(0xFFF5F5F5),
color: c.textSecondary,
backgroundColor: c.surface,
),
),
);
}
// ---------- Build ----------
@override
Widget build(BuildContext context) {
final c = context.colors;
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(
title: Text(
'更新日志',
style: TextStyle(
fontFamily: 'HarmonyOS_Sans_SC',
color: Colors.black,
color: c.textPrimary,
),
),
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),
),
backgroundColor: c.divider,
valueColor: AlwaysStoppedAnimation<Color>(c.progressBar),
minHeight: 2.0,
),
),
@@ -188,16 +175,16 @@ class _ChangelogPageState extends State<ChangelogPage>
? Center(
child: Text(
_error!,
style: const TextStyle(
style: TextStyle(
fontFamily: 'HarmonyOS_Sans_SC',
color: Colors.grey,
color: c.textTertiary,
),
),
)
: SingleChildScrollView(
controller: _scrollController,
padding: const EdgeInsets.all(16.0),
child: _cachedMarkdown ?? const SizedBox.shrink(),
child: _buildMarkdown(context, _markdownContent),
),
);
}
+57
View File
@@ -0,0 +1,57 @@
import 'package:flutter/services.dart';
/// BatteryManager 实时数据(电池端:电压 / 电流 / 功率)
class BatteryManagerData {
final int voltageMv;
final int currentUa;
final double powerW;
final double temperatureC;
final int plugged; // 0=未插, 1=AC, 2=USB, 4=WIRELESS
final bool isCharging;
const BatteryManagerData({
required this.voltageMv,
required this.currentUa,
required this.powerW,
required this.temperatureC,
required this.plugged,
required this.isCharging,
});
factory BatteryManagerData.empty() => const BatteryManagerData(
voltageMv: 0,
currentUa: 0,
powerW: 0,
temperatureC: 0,
plugged: 0,
isCharging: false,
);
factory BatteryManagerData.fromMap(dynamic raw) {
if (raw is! Map) return BatteryManagerData.empty();
return BatteryManagerData(
voltageMv: (raw['voltageMv'] as num?)?.toInt() ?? 0,
currentUa: (raw['currentUa'] as num?)?.toInt() ?? 0,
powerW: (raw['powerW'] as num?)?.toDouble() ?? 0.0,
temperatureC: (raw['temperatureC'] as num?)?.toDouble() ?? 0.0,
plugged: (raw['plugged'] as num?)?.toInt() ?? 0,
isCharging: raw['isCharging'] as bool? ?? false,
);
}
/// 电压 V
double get voltageV => voltageMv / 1000.0;
/// 电流 A
double get currentA => currentUa / 1_000_000.0;
}
class BatteryManagerService {
static const _channel = MethodChannel('com.lxh.battery_health/charge');
/// 主动拉取一次
static Future<BatteryManagerData> fetch() async {
final result = await _channel.invokeMethod('getBatteryManagerData');
return BatteryManagerData.fromMap(result);
}
}
+164
View File
@@ -0,0 +1,164 @@
import 'package:flutter/material.dart';
@immutable
class AppColors extends ThemeExtension<AppColors> {
final Color background; // 页面背景
final Color surface; // 卡片/面板背景
final Color textPrimary; // 主文字
final Color textSecondary; // 次要文字
final Color textTertiary; // 弱化文字
final Color divider; // 分割线
final Color icon; // 图标色
final Color healthGreen; // 健康度 ≥80%
final Color healthYellow; // 70~80%
final Color healthOrange;
final Color healthRed; // <70% 或异常
final Color barEmpty; // 无数据柱
final Color voltUp; // 电压↑
final Color voltDown; // 电压↓
final Color tempUp;
final Color tempDown;
final Color progressBar; // 更新日志进度条
final Color statusBarTint; // 状态栏背景
const AppColors({
required this.background,
required this.surface,
required this.textPrimary,
required this.textSecondary,
required this.textTertiary,
required this.divider,
required this.icon,
required this.healthGreen,
required this.healthYellow,
required this.healthOrange,
required this.healthRed,
required this.barEmpty,
required this.voltUp,
required this.voltDown,
required this.tempUp,
required this.tempDown,
required this.progressBar,
required this.statusBarTint,
});
@override
AppColors copyWith({
Color? background,
Color? surface,
Color? textPrimary,
Color? textSecondary,
Color? textTertiary,
Color? divider,
Color? icon,
Color? healthGreen,
Color? healthYellow,
Color? healthOrange,
Color? healthRed,
Color? barEmpty,
Color? voltUp,
Color? voltDown,
Color? tempUp,
Color? tempDown,
Color? progressBar,
Color? statusBarTint,
}) {
return AppColors(
background: background ?? this.background,
surface: surface ?? this.surface,
textPrimary: textPrimary ?? this.textPrimary,
textSecondary: textSecondary ?? this.textSecondary,
textTertiary: textTertiary ?? this.textTertiary,
divider: divider ?? this.divider,
icon: icon ?? this.icon,
healthGreen: healthGreen ?? this.healthGreen,
healthYellow: healthYellow ?? this.healthYellow,
healthOrange: healthOrange ?? this.healthOrange,
healthRed: healthRed ?? this.healthRed,
barEmpty: barEmpty ?? this.barEmpty,
voltUp: voltUp ?? this.voltUp,
voltDown: voltDown ?? this.voltDown,
tempUp: tempUp ?? this.tempUp,
tempDown: tempDown ?? this.tempDown,
progressBar: progressBar ?? this.progressBar,
statusBarTint: statusBarTint ?? this.statusBarTint,
);
}
@override
AppColors lerp(ThemeExtension<AppColors>? other, double t) {
if (other is! AppColors) return this;
return AppColors(
background: Color.lerp(background, other.background, t)!,
surface: Color.lerp(surface, other.surface, t)!,
textPrimary: Color.lerp(textPrimary, other.textPrimary, t)!,
textSecondary: Color.lerp(textSecondary, other.textSecondary, t)!,
textTertiary: Color.lerp(textTertiary, other.textTertiary, t)!,
divider: Color.lerp(divider, other.divider, t)!,
icon: Color.lerp(icon, other.icon, t)!,
healthGreen: Color.lerp(healthGreen, other.healthGreen, t)!,
healthYellow: Color.lerp(healthYellow, other.healthYellow, t)!,
healthOrange: Color.lerp(healthOrange, other.healthOrange, t)!,
healthRed: Color.lerp(healthRed, other.healthRed, t)!,
barEmpty: Color.lerp(barEmpty, other.barEmpty, t)!,
voltUp: Color.lerp(voltUp, other.voltUp, t)!,
voltDown: Color.lerp(voltDown, other.voltDown, t)!,
tempUp: Color.lerp(tempUp, other.tempUp, t)!,
tempDown: Color.lerp(tempDown, other.tempDown, t)!,
progressBar: Color.lerp(progressBar, other.progressBar, t)!,
statusBarTint: Color.lerp(statusBarTint, other.statusBarTint, t)!,
);
}
}
/// 浅色主题(保持现有视觉)
const AppColors kLightColors = AppColors(
background: Colors.white,
surface: Color(0xFFF0F4F8),
textPrimary: Colors.black,
textSecondary: Color(0xFF5F5F5F),
textTertiary: Colors.grey,
divider: Color(0xFFE0E0E0),
icon: Colors.black,
healthGreen: Color(0xFF00B140),
healthYellow: Color(0xFFEFDF00),
healthOrange: Color(0xFFD75A16),
healthRed: Colors.red,
barEmpty: Color(0xFFE0E0E0),
voltUp: Colors.red,
voltDown: Colors.blue,
tempUp: Colors.red,
tempDown: Colors.blue,
progressBar: Color.fromRGBO(157, 212, 237, 1.0),
statusBarTint: Color.fromRGBO(157, 212, 237, 0.1),
);
/// 深色主题(色相保留,明度调整)
const AppColors kDarkColors = AppColors(
background: Color(0xFF121212),
surface: Color(0xFF1E1E1E),
textPrimary: Color(0xFFE8E8E8),
textSecondary: Color(0xFFA0A0A0),
textTertiary: Color(0xFF707070),
divider: Color(0xFF2A2A2A),
icon: Color(0xFFE8E8E8),
healthGreen: Color(0xFF00D14D),
healthYellow: Color(0xFFD4C520),
healthOrange: Color(0xFFFF7838),
healthRed: Color(0xFFFF5252),
barEmpty: Color(0xFF2A2A2A),
voltUp: Color(0xFFFF5252),
voltDown: Color(0xFF5AA9FF),
tempUp: Color(0xFFFF5252),
tempDown: Color(0xFF5AA9FF),
progressBar: Color(0xFF5A9BBD),
statusBarTint: Color(0xFF121212),
);
/// 便捷扩展:context.colors.textPrimary
extension AppColorsX on BuildContext {
AppColors get colors => Theme.of(this).extension<AppColors>()!;
}
+45
View File
@@ -0,0 +1,45 @@
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
class ThemeService {
static const _prefKey = 'theme_mode';
static final ValueNotifier<ThemeMode> notifier = ValueNotifier(
ThemeMode.system,
);
/// 启动时调用(main.dart)
static Future<void> init() async {
final prefs = await SharedPreferences.getInstance();
final saved = prefs.getString(_prefKey);
notifier.value = _parse(saved);
}
/// 菜单里调用
static Future<void> setMode(ThemeMode mode) async {
notifier.value = mode;
final prefs = await SharedPreferences.getInstance();
await prefs.setString(_prefKey, _name(mode));
}
static ThemeMode _parse(String? s) {
switch (s) {
case 'light':
return ThemeMode.light;
case 'dark':
return ThemeMode.dark;
default:
return ThemeMode.system;
}
}
static String _name(ThemeMode m) {
switch (m) {
case ThemeMode.light:
return 'light';
case ThemeMode.dark:
return 'dark';
case ThemeMode.system:
return 'system';
}
}
}
+44 -20
View File
@@ -1,7 +1,7 @@
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,7 +13,7 @@ class ChargeUtils {
return '≈${matchedLevel}W';
}
// ============ 原有有线逻辑(保留兼容,但主界面不再直接调用) ============
// ============ 旧的有线逻辑(保留兼容) ============
static String getDisplayPowerWired(
double power,
int? wiredLevel,
@@ -34,57 +34,81 @@ 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 (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/${requested}W";
}
// 无档位信息,仅显示实际功率
return "≈${actual}w";
return "≈${actual}W";
}
// ============ 🟢 1.5.0 新增:二级页面标定档位显示 ============
/// 示例:≈80w/L-10
// ============ 主界面:BM 电池端功率 + 标定档位 ============
/// 示例:5.98W / 7.5W、12.3W、读取中...
static String getMainDisplayPowerBm(
double bmPower,
int? wiredLevel,
String chargeType,
bool isWireless,
) {
if (bmPower <= 0 || bmPower > 100) return '读取中...';
final powerStr = '${bmPower.toStringAsFixed(1)}W';
if (isWireless || chargeType == 'SDP') return powerStr;
if (wiredLevel != null && ChargeMaps.wiredPower.containsKey(wiredLevel)) {
final requested = ChargeMaps.wiredPower[wiredLevel]!;
final requestedStr = requested == requested.roundToDouble()
? requested.toInt().toString()
: requested.toStringAsFixed(1);
return '$powerStr / ${requestedStr}W';
}
return powerStr;
}
// ============ 详情页:标定档位 ============
/// 示例:≈80W/L-10、≈7.5W/L-1
static String getDetailRequestedPowerWired(
double power,
int? wiredLevel,
String chargeType,
) {
if (chargeType == "SDP") return "≈7.5w";
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";
final requested = ChargeMaps.wiredPower[wiredLevel]!;
final str = requested == requested.roundToDouble()
? requested.toInt().toString()
: requested.toStringAsFixed(1);
return "≈${str}W/L-$wiredLevel";
}
return "≈${power.round()}w";
return "≈${power.round()}W";
}
// ============ 🟢 1.5.0 新增:二级页面实际功率显示 ============
// ============ 详情页:实际功率 ============
static String getDetailActualPowerWired(double power, String chargeType) {
if (chargeType == "SDP") return "≈7.5w";
if (chargeType == "SDP") return "≈7.5W";
if (power == 0) return "读取中...";
return "≈${power.round()}w";
return "≈${power.round()}W";
}
// ============ 充电类型标签(保持不变) ============
// ============ 充电类型标签 ============
static String getChargeTypeLabel(bool isWireless, String chargeType) {
if (isWireless) {
return chargeType == "无线" ? "无线" : chargeType;
+43 -45
View File
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import '../models/data_abnormal_type.dart';
import '../theme/app_colors.dart';
class BatteryInfoPanel extends StatelessWidget {
final double batteryVolt;
@@ -9,9 +10,8 @@ class BatteryInfoPanel extends StatelessWidget {
final double minVolt;
final double maxTemp;
final double minTemp;
final DataAbnormalType abnormalType;
final double tempAvg;
final DataAbnormalType abnormalType;
const BatteryInfoPanel({
super.key,
@@ -27,19 +27,22 @@ class BatteryInfoPanel extends StatelessWidget {
@override
Widget build(BuildContext context) {
final c = context.colors;
final bool isVoltageAbnormal =
abnormalType == DataAbnormalType.voltageAbnormal;
final Color voltColor = isVoltageAbnormal ? Colors.red : Colors.black;
final Color voltColor = isVoltageAbnormal ? c.healthRed : c.textPrimary;
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// ---------- 电压列 ----------
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildInfoItem(
_buildIcon('voltage'),
context,
_buildIcon(context, 'voltage'),
'电压',
'${batteryVolt.toStringAsFixed(3)}V',
textColor: voltColor,
@@ -53,22 +56,17 @@ class BatteryInfoPanel extends StatelessWidget {
fontFamily: 'Flyme',
fontSize: 14,
fontWeight: FontWeight.w500,
color: Colors.red,
color: c.voltUp,
),
children: [
const TextSpan(
TextSpan(
text: '↑ ',
style: TextStyle(
fontFamily: 'HarmonyOS_Sans_SC',
color: Colors.red,
),
),
TextSpan(
text: '${maxVolt.toStringAsFixed(3)}V',
style: TextStyle(
color: isVoltageAbnormal ? Colors.red : Colors.red,
color: c.voltUp,
),
),
TextSpan(text: '${maxVolt.toStringAsFixed(3)}V'),
],
),
),
@@ -79,22 +77,17 @@ class BatteryInfoPanel extends StatelessWidget {
fontFamily: 'Flyme',
fontSize: 14,
fontWeight: FontWeight.w500,
color: Colors.blue,
color: c.voltDown,
),
children: [
const TextSpan(
TextSpan(
text: '↓ ',
style: TextStyle(
fontFamily: 'HarmonyOS_Sans_SC',
color: Colors.blue,
),
),
TextSpan(
text: '${minVolt.toStringAsFixed(3)}V',
style: TextStyle(
color: isVoltageAbnormal ? Colors.red : Colors.blue,
color: c.voltDown,
),
),
TextSpan(text: '${minVolt.toStringAsFixed(3)}V'),
],
),
),
@@ -103,33 +96,34 @@ class BatteryInfoPanel extends StatelessWidget {
],
),
),
// ---------- 温度列 ----------
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildInfoItem(
_buildIcon('temperature'),
context,
_buildIcon(context, 'temperature'),
'温度',
'${batteryTemp.toStringAsFixed(0)}℃',
),
const SizedBox(height: 8),
// 🆕 一行显示:↑ max ↓ min [avg] avg
Row(
children: [
RichText(
text: TextSpan(
style: const TextStyle(
style: TextStyle(
fontFamily: 'Flyme',
fontSize: 12,
fontWeight: FontWeight.w500,
color: Colors.red,
color: c.tempUp,
),
children: [
const TextSpan(
TextSpan(
text: '↑ ',
style: TextStyle(
fontFamily: 'HarmonyOS_Sans_SC',
color: Colors.red,
color: c.tempUp,
),
),
TextSpan(text: '${maxTemp.toStringAsFixed(1)}℃'),
@@ -139,18 +133,18 @@ class BatteryInfoPanel extends StatelessWidget {
const SizedBox(width: 6),
RichText(
text: TextSpan(
style: const TextStyle(
style: TextStyle(
fontFamily: 'Flyme',
fontSize: 12,
fontWeight: FontWeight.w500,
color: Colors.blue,
color: c.tempDown,
),
children: [
const TextSpan(
TextSpan(
text: '↓ ',
style: TextStyle(
fontFamily: 'HarmonyOS_Sans_SC',
color: Colors.blue,
color: c.tempDown,
),
),
TextSpan(text: '${minTemp.toStringAsFixed(1)}℃'),
@@ -158,15 +152,15 @@ class BatteryInfoPanel extends StatelessWidget {
),
),
const SizedBox(width: 6),
_buildIcon('avg', size: 14),
_buildIcon(context, 'avg', size: 14),
const SizedBox(width: 2),
Text(
'${tempAvg.toStringAsFixed(1)}℃',
style: const TextStyle(
style: TextStyle(
fontFamily: 'Flyme',
fontSize: 12,
fontWeight: FontWeight.w500,
color: Colors.grey,
color: c.textTertiary,
),
),
],
@@ -178,50 +172,54 @@ class BatteryInfoPanel extends StatelessWidget {
);
}
Widget _buildIcon(String type, {double size = 18}) {
Widget _buildIcon(BuildContext context, String type, {double size = 18}) {
final c = context.colors;
final filter = ColorFilter.mode(c.textTertiary, BlendMode.srcIn);
switch (type) {
case 'voltage':
return SvgPicture.asset(
'assets/icons/si--lightning-fill.svg',
width: size,
height: size,
colorFilter: const ColorFilter.mode(Colors.grey, BlendMode.srcIn),
colorFilter: filter,
);
case 'temperature':
return SvgPicture.asset(
'assets/icons/mdi--temperature.svg',
width: size,
height: size,
colorFilter: const ColorFilter.mode(Colors.grey, BlendMode.srcIn),
colorFilter: filter,
);
case 'avg': // 🆕
case 'avg':
return SvgPicture.asset(
'assets/icons/avg.svg',
width: size,
height: size,
colorFilter: const ColorFilter.mode(Colors.grey, BlendMode.srcIn),
colorFilter: filter,
);
default:
return Icon(Icons.help_outline, size: size, color: Colors.grey);
return Icon(Icons.help_outline, size: size, color: c.textTertiary);
}
}
Widget _buildInfoItem(
BuildContext context,
Widget icon,
String label,
String value, {
Color textColor = Colors.black,
Color? textColor,
}) {
final c = context.colors;
return Row(
children: [
icon,
const SizedBox(width: 6),
Text(
label,
style: const TextStyle(
style: TextStyle(
fontFamily: 'HarmonyOS_Sans_SC',
fontSize: 14,
color: Colors.grey,
color: c.textTertiary,
),
),
const SizedBox(width: 6),
@@ -231,7 +229,7 @@ class BatteryInfoPanel extends StatelessWidget {
fontFamily: 'Flyme',
fontSize: 16,
fontWeight: FontWeight.w500,
color: textColor,
color: textColor ?? c.textPrimary,
),
),
],
+13 -15
View File
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import '../models/data_abnormal_type.dart';
import '../theme/app_colors.dart';
class BatterySummary extends StatelessWidget {
final double healthPercent;
@@ -19,25 +20,23 @@ class BatterySummary extends StatelessWidget {
@override
Widget build(BuildContext context) {
final c = context.colors;
final bool isAbnormal = abnormalType != DataAbnormalType.none;
final bool isCapacityAbnormal =
abnormalType == DataAbnormalType.capacityTooLow ||
abnormalType == DataAbnormalType.capacityTooHigh;
// 健康度颜色:异常时红色,否则按阈值
final Color healthColor = isAbnormal
? Colors.red
: (healthPercent < 70 ? Colors.red : Colors.black);
? c.healthRed
: (healthPercent < 70 ? c.healthRed : c.textPrimary);
// 容量颜色:异常时红色,否则灰色
final Color capacityColor = isCapacityAbnormal
? Colors.red
: const Color(0xFF5F5F5F);
? c.healthRed
: c.textSecondary;
return Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
// 健康度大数字
Row(
crossAxisAlignment: CrossAxisAlignment.baseline,
textBaseline: TextBaseline.alphabetic,
@@ -60,34 +59,33 @@ class BatterySummary extends StatelessWidget {
fontSize: 20,
fontWeight: FontWeight.w300,
height: 1.0,
color: healthColor == Colors.red
? Colors.red
: Colors.grey.shade600,
color: healthColor == c.healthRed
? c.healthRed
: c.textSecondary,
),
),
],
),
const Spacer(),
// cycles + mAh
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
RichText(
text: TextSpan(
style: const TextStyle(
style: TextStyle(
fontFamily: 'Flyme',
fontSize: 16,
fontWeight: FontWeight.w500,
height: 1.2,
color: Colors.black,
color: c.textPrimary,
),
children: [
TextSpan(text: '$cycleCount'),
const TextSpan(
TextSpan(
text: ' cycles',
style: TextStyle(
fontFamily: 'HarmonyOS_Sans_SC',
color: Colors.black,
color: c.textPrimary,
),
),
],
+88 -45
View File
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../utils/charge_utils.dart';
import '../theme/app_colors.dart';
class ChargeDetailPage extends StatelessWidget {
final String chargeType;
@@ -9,6 +10,9 @@ class ChargeDetailPage extends StatelessWidget {
final double volt;
final double curr;
final int? wiredLevel;
final double bmPower;
final double bmVolt;
final double bmCurr;
const ChargeDetailPage({
super.key,
@@ -18,6 +22,9 @@ class ChargeDetailPage extends StatelessWidget {
required this.volt,
required this.curr,
this.wiredLevel,
this.bmPower = 0,
this.bmVolt = 0,
this.bmCurr = 0,
});
String _getProtocolDisplay() {
@@ -41,61 +48,71 @@ class ChargeDetailPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final c = context.colors;
final isDark = Theme.of(context).brightness == Brightness.dark;
return AnnotatedRegion<SystemUiOverlayStyle>(
value: const SystemUiOverlayStyle(
statusBarColor: Color.fromRGBO(157, 212, 237, 0.1),
statusBarIconBrightness: Brightness.dark,
statusBarBrightness: Brightness.dark,
value: SystemUiOverlayStyle(
statusBarColor: c.statusBarTint,
statusBarIconBrightness: isDark ? Brightness.light : Brightness.dark,
statusBarBrightness: Theme.of(context).brightness,
),
child: Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
title: const Text('充电详情'),
backgroundColor: Colors.white,
backgroundColor: c.background,
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()),
// ========== BMS 来源 ==========
_buildSectionTitle(context, 'BMS 日志'),
_buildTextRow(context, '协议', _getProtocolDisplay()),
const Divider(),
if (!isWireless) ...[
_buildTextRow('标定档位', _getRequestedDisplay()),
_buildTextRow(
context,
'标定档位',
_getRequestedDisplay(),
valueFont: 'Flyme',
),
const Divider(),
_buildTextRow('实际功率', _getActualDisplay()),
_buildTextRow(
context,
'实际功率',
_getActualDisplay(),
valueFont: 'Flyme',
),
const Divider(),
] else ...[
_buildTextRow('功率', ChargeUtils.getDisplayPowerWireless(power)),
_buildTextRow(
context,
'功率',
ChargeUtils.getDisplayPowerWireless(power),
valueFont: 'Flyme',
),
const Divider(),
],
_buildDetailRow('电压', volt, 'V'),
_buildDetailRow(context, '电压', volt, 'V'),
const Divider(),
_buildDetailRow('电流', curr, 'A'),
_buildDetailRow(context, '电流', curr, 'A'),
const Divider(),
if (isWireless) _buildTextRow('类型', '无线'),
if (!isWireless && wiredLevel == null) _buildTextRow('档位', '自动'),
const SizedBox(height: 24),
// ========== BM 来源 ==========
_buildSectionTitle(context, 'BatteryManager(电池端)'),
_buildDetailRow(context, '电压', bmVolt, 'V'),
const Divider(),
_buildDetailRow(context, '电流', bmCurr, 'A'),
const Divider(),
bmPower > 0
? _buildDetailRow(context, '功率', bmPower, 'W')
: _buildTextRow(context, '功率', '读取中...'),
const Spacer(),
Center(
child: Text(
'数据来自 BMS 实时日志',
style: TextStyle(
fontFamily: 'HarmonyOS_Sans_SC',
fontSize: 12,
color: Colors.grey.shade500,
),
),
),
],
),
),
@@ -103,8 +120,29 @@ class ChargeDetailPage extends StatelessWidget {
);
}
/// 纯文本行(协议、功率、档位等)
Widget _buildTextRow(String label, String value) {
Widget _buildSectionTitle(BuildContext context, String title) {
final c = context.colors;
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Text(
title,
style: TextStyle(
fontFamily: 'HarmonyOS_Sans_SC',
fontSize: 13,
fontWeight: FontWeight.w600,
color: c.textTertiary,
),
),
);
}
Widget _buildTextRow(
BuildContext context,
String label,
String value, {
String valueFont = 'HarmonyOS_Sans_SC',
}) {
final c = context.colors;
return Padding(
padding: const EdgeInsets.symmetric(vertical: 6.0),
child: Row(
@@ -112,20 +150,20 @@ class ChargeDetailPage extends StatelessWidget {
children: [
Text(
label,
style: const TextStyle(
style: TextStyle(
fontFamily: 'HarmonyOS_Sans_SC',
fontSize: 16,
fontWeight: FontWeight.w400,
color: Colors.grey,
color: c.textTertiary,
),
),
Text(
value,
style: const TextStyle(
fontFamily: 'HarmonyOS_Sans_SC', // 复杂字符串保持 Harmony
style: TextStyle(
fontFamily: valueFont,
fontSize: 16,
fontWeight: FontWeight.w500,
color: Colors.black87,
color: c.textPrimary,
),
),
],
@@ -133,8 +171,13 @@ class ChargeDetailPage extends StatelessWidget {
);
}
/// 数值行(电压/电流,数字用 Flyme,单位用 Harmony)
Widget _buildDetailRow(String label, double value, String unit) {
Widget _buildDetailRow(
BuildContext context,
String label,
double value,
String unit,
) {
final c = context.colors;
return Padding(
padding: const EdgeInsets.symmetric(vertical: 6.0),
child: Row(
@@ -142,24 +185,24 @@ class ChargeDetailPage extends StatelessWidget {
children: [
Text(
label,
style: const TextStyle(
style: TextStyle(
fontFamily: 'HarmonyOS_Sans_SC',
fontSize: 16,
fontWeight: FontWeight.w400,
color: Colors.grey,
color: c.textTertiary,
),
),
RichText(
text: TextSpan(
style: const TextStyle(
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: Colors.black87,
color: c.textPrimary,
),
children: [
TextSpan(
text: value.toStringAsFixed(2),
style: const TextStyle(fontFamily: 'Flyme'), // 🟢 数字用 Flyme
style: const TextStyle(fontFamily: 'Flyme'),
),
TextSpan(
text: unit,
+65 -9
View File
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import '../theme/app_colors.dart';
class ChargeEntry extends StatelessWidget {
final String summary;
@@ -14,18 +15,23 @@ class ChargeEntry extends StatelessWidget {
@override
Widget build(BuildContext context) {
final c = context.colors;
return GestureDetector(
onTap: isCharging ? onTap : null,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
summary,
style: const TextStyle(
fontFamily: 'HarmonyOS_Sans_SC', // 🟢 混合文本用 Harmony
fontSize: 16,
fontWeight: FontWeight.w500,
color: Colors.black87,
Flexible(
child: RichText(
overflow: TextOverflow.ellipsis,
text: TextSpan(
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: c.textPrimary,
),
children: _buildSpans(summary),
),
),
),
Row(
@@ -36,7 +42,7 @@ class ChargeEntry extends StatelessWidget {
fontFamily: 'HarmonyOS_Sans_SC',
fontSize: 16,
fontWeight: FontWeight.w500,
color: Colors.grey.shade700,
color: c.textTertiary,
),
),
const SizedBox(width: 4),
@@ -46,7 +52,7 @@ class ChargeEntry extends StatelessWidget {
fontFamily: 'HarmonyOS_Sans_SC',
fontSize: 16,
fontWeight: FontWeight.w500,
color: Colors.grey.shade700,
color: c.textTertiary,
),
),
],
@@ -55,4 +61,54 @@ class ChargeEntry extends StatelessWidget {
),
);
}
/// 按 CJK / 非 CJK 拆分字符串,分别应用 HarmonyOS / Flyme
List<TextSpan> _buildSpans(String text) {
final spans = <TextSpan>[];
if (text.isEmpty) return spans;
final buffer = StringBuffer();
bool? isCJK;
void flush() {
if (buffer.isEmpty) return;
spans.add(
TextSpan(
text: buffer.toString(),
style: TextStyle(
fontFamily: (isCJK ?? false) ? 'HarmonyOS_Sans_SC' : 'Flyme',
),
),
);
buffer.clear();
}
for (final rune in text.runes) {
final cjk = _isCJKRune(rune);
if (isCJK == null || cjk == isCJK) {
buffer.writeCharCode(rune);
isCJK = cjk;
} else {
flush();
buffer.writeCharCode(rune);
isCJK = cjk;
}
}
flush();
return spans;
}
bool _isCJKRune(int rune) {
// CJK 统一表意文字
if (rune >= 0x4E00 && rune <= 0x9FFF) return true;
// CJK 扩展 A
if (rune >= 0x3400 && rune <= 0x4DBF) return true;
// 中文标点(。、,:;等)
if (rune >= 0x3000 && rune <= 0x303F) return true;
// CJK 兼容符号
if (rune >= 0xFE30 && rune <= 0xFE4F) return true;
// 全角字符
if (rune >= 0xFF00 && rune <= 0xFFEF) return true;
return false;
}
}
+19 -21
View File
@@ -1,11 +1,6 @@
import 'package:flutter/material.dart';
import '../theme/app_colors.dart';
// --- 潘通色定义 ---
const Color kHealthGreen = Color(0xFF00B140);
const Color kHealthYellow = Color(0xFFEFDF00);
const Color kHealthOrange = Color(0xFFD75A16);
/// 趋势图柱状图组件
class TrendChart extends StatelessWidget {
final List<Map<String, dynamic>> data;
final int designCap;
@@ -14,6 +9,8 @@ class TrendChart extends StatelessWidget {
@override
Widget build(BuildContext context) {
final c = context.colors;
List<Map<String, dynamic>> displayData = data.isNotEmpty
? data
: _getMockData();
@@ -48,12 +45,13 @@ class TrendChart extends StatelessWidget {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
Text(
'容量百分比变动趋势(近15天)',
style: TextStyle(
fontFamily: 'HarmonyOS_Sans_SC',
fontSize: 16,
fontWeight: FontWeight.w500,
color: c.textPrimary,
),
),
const SizedBox(height: 8),
@@ -64,13 +62,13 @@ class TrendChart extends StatelessWidget {
height: 180,
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: const [
children: [
Text(
'100',
style: TextStyle(
fontFamily: 'Flyme',
fontSize: 10,
color: Colors.grey,
color: c.textTertiary,
height: 1.0,
),
),
@@ -79,7 +77,7 @@ class TrendChart extends StatelessWidget {
style: TextStyle(
fontFamily: 'Flyme',
fontSize: 10,
color: Colors.grey,
color: c.textTertiary,
height: 1.0,
),
),
@@ -106,14 +104,14 @@ class TrendChart extends StatelessWidget {
Color barColor;
if (value > 0) {
if (value >= 80) {
barColor = kHealthGreen;
barColor = c.healthGreen;
} else if (value >= 70) {
barColor = kHealthYellow;
barColor = c.healthYellow;
} else {
barColor = Colors.red;
barColor = c.healthRed;
}
} else {
barColor = Colors.grey.shade300;
barColor = c.barEmpty;
}
double clampedValue = value.clamp(70.0, 100.0);
double heightFactor =
@@ -144,10 +142,10 @@ class TrendChart extends StatelessWidget {
bottom: 2,
child: Text(
sevenDaysAgoDate,
style: const TextStyle(
style: TextStyle(
fontFamily: 'HarmonyOS_Sans_SC',
fontSize: 9,
color: Colors.grey,
color: c.textTertiary,
height: 1.0,
),
),
@@ -158,10 +156,10 @@ class TrendChart extends StatelessWidget {
bottom: 2,
child: Text(
todayDate,
style: const TextStyle(
style: TextStyle(
fontFamily: 'HarmonyOS_Sans_SC',
fontSize: 9,
color: Colors.black,
color: c.textPrimary,
height: 1.0,
fontWeight: FontWeight.w600,
),
@@ -176,14 +174,14 @@ class TrendChart extends StatelessWidget {
],
),
if (percents.every((v) => v == 0))
const Padding(
padding: EdgeInsets.only(top: 8.0),
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Text(
'暂无历史数据,请点击刷新按钮更新',
style: TextStyle(
fontFamily: 'HarmonyOS_Sans_SC',
fontSize: 12,
color: Colors.grey,
color: c.textTertiary,
),
),
),
+1 -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
# 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.7.2
version: 1.8.0
environment:
sdk: ^3.9.2